Bounties - refactor tests (#4589)
This commit is contained in:
parent
54567442c1
commit
cb954bc628
@ -21,6 +21,7 @@ const defaultConfig = {
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
|
||||
'\\.(md)$': '<rootDir>/jest/mocks/empty.js'
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/jest/jest-setup.ts'],
|
||||
transformIgnorePatterns: [
|
||||
'<rootDir>/node_modules'
|
||||
]
|
||||
|
||||
11
jest/jest-setup.ts
Normal file
11
jest/jest-setup.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright 2017-2021 @polkadot/apps authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-global-assign
|
||||
CSS = { supports (): boolean {
|
||||
return false;
|
||||
} };
|
||||
497
packages/page-bounties/src/Bounties.spec.ts
Normal file
497
packages/page-bounties/src/Bounties.spec.ts
Normal file
@ -0,0 +1,497 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import i18next from '@polkadot/react-components/i18n';
|
||||
import { createAugmentedApi } from '@polkadot/test-support/api';
|
||||
import { balanceOf } from '@polkadot/test-support/creation/balance';
|
||||
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
|
||||
import { proposalFactory } from '@polkadot/test-support/creation/treasury/proposalFactory';
|
||||
import { mockHooks } from '@polkadot/test-support/hooks/mockHooks';
|
||||
import { MemoryStore } from '@polkadot/test-support/keyring';
|
||||
import { alice, bob } from '@polkadot/test-support/keyring/addresses';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
|
||||
import { defaultBountyUpdatePeriod, mockBountyHooks } from '../test/hooks/defaults';
|
||||
import { BountiesPage } from '../test/pages/bountiesPage';
|
||||
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyInfos';
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useTreasury', () => ({
|
||||
useTreasury: () => mockHooks.treasury
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useMembers', () => ({
|
||||
useMembers: () => mockHooks.members
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useBlockTime', () => ({
|
||||
useBlockTime: () => mockHooks.blockTime
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/useBalance', () => ({
|
||||
useBalance: () => mockBountyHooks.balance
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/useBounties', () => ({
|
||||
useBounties: () => mockBountyHooks.bountyApi
|
||||
}));
|
||||
|
||||
let aProposal: (extrinsic: SubmittableExtrinsic<'promise'>, ayes?: string[], nays?: string[]) => DeriveCollectiveProposal;
|
||||
let augmentedApi: ApiPromise;
|
||||
let aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
|
||||
let aBountyIndex: (index?:number) => BountyIndex;
|
||||
let bountyStatusWith: ({ curator, status, updateDue }: { curator?: string, status?: string, updateDue?: number}) => BountyStatus;
|
||||
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
|
||||
describe('Bounties', () => {
|
||||
let bountiesPage: BountiesPage;
|
||||
|
||||
beforeAll(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
|
||||
augmentedApi = createAugmentedApi();
|
||||
({ aBounty, aBountyIndex, bountyStatusWith, bountyWith } = new BountyFactory(augmentedApi));
|
||||
({ aProposal } = proposalFactory(augmentedApi));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
bountiesPage = new BountiesPage(augmentedApi);
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('shows message when no bounties', async () => {
|
||||
bountiesPage.renderMany();
|
||||
|
||||
await bountiesPage.expectText('No open bounties');
|
||||
});
|
||||
|
||||
it('renders a bounty', async () => {
|
||||
bountiesPage.renderOne(aBounty(), [], 'kusama comic book');
|
||||
|
||||
await bountiesPage.expectText('kusama comic book');
|
||||
bountiesPage.expectTextAbsent('No open bounties');
|
||||
});
|
||||
|
||||
it('renders bounties in order from newest to oldest', async () => {
|
||||
bountiesPage.renderMany({
|
||||
bounties: [
|
||||
{ bounty: aBounty(), description: 'bounty 2', index: aBountyIndex(2), proposals: [] },
|
||||
{ bounty: aBounty(), description: 'bounty 1', index: aBountyIndex(1), proposals: [] },
|
||||
{ bounty: aBounty(), description: 'bounty 3', index: aBountyIndex(3), proposals: [] }
|
||||
]
|
||||
});
|
||||
|
||||
expect(await bountiesPage.findAllDescriptions()).toEqual(['bounty 3', 'bounty 2', 'bounty 1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bounty in a list', () => {
|
||||
describe('has extended status', () => {
|
||||
it('when voting on proposed curator', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Curator under voting');
|
||||
});
|
||||
|
||||
it('when voting on bounty approval', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty approval under voting');
|
||||
});
|
||||
|
||||
it('when simultaneous close and approve motions exist, show approved', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [
|
||||
aProposal(augmentedApi.tx.bounties.closeBounty(0)),
|
||||
aProposal(augmentedApi.tx.bounties.approveBounty(0))
|
||||
];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty approval under voting');
|
||||
});
|
||||
|
||||
it('when voting on close bounty', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty rejection under voting');
|
||||
});
|
||||
|
||||
it('when voting on unassign curator', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.unassignCurator(0))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Curator slash under voting');
|
||||
});
|
||||
|
||||
it('when a motion exists that would fail on execution, show nothing', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extendedVotingStatus')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('when bounty is claimable', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty);
|
||||
|
||||
expect((await findByTestId('extendedActionStatus')).textContent).toEqual('Claimable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('has extended description for Curator', () => {
|
||||
it('when propose curator motion is voted and bounty is in Funded state', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.proposeCurator(0, alice, 1))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Proposed Curator');
|
||||
});
|
||||
|
||||
it('when bounty is in Funded status, but there is no motion, show nothing', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.rendered();
|
||||
bountiesPage.expectTextAbsent('Proposed Curator');
|
||||
});
|
||||
|
||||
it('when status is different, show nothing', async () => {
|
||||
const bounty = bountyWith({ status: 'CuratorProposed' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.rendered();
|
||||
bountiesPage.expectTextAbsent('Proposed Curator');
|
||||
});
|
||||
});
|
||||
|
||||
describe('has Beneficiary description', () => {
|
||||
it('in PendingPayout status', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.awardBounty(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z'))];
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Beneficiary');
|
||||
});
|
||||
|
||||
it('not in other status', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.rendered();
|
||||
bountiesPage.expectTextAbsent('Beneficiary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('has voting summary', () => {
|
||||
it('is displayed when voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting results');
|
||||
});
|
||||
|
||||
it('is not displayed when not voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals: DeriveCollectiveProposal[] = [];
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('voting-summary')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('has voters', () => {
|
||||
it('aye and nay', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
const { findAllByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
|
||||
const nayVoters = await findAllByTestId((testId) => testId.startsWith('voters_nays'));
|
||||
|
||||
expect(ayeVoters).toHaveLength(1);
|
||||
expect(nayVoters).toHaveLength(1);
|
||||
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
|
||||
expect(nayVoters[0].getAttribute('data-testid')).toContain(bob);
|
||||
});
|
||||
|
||||
it('multiple ayes and no nay', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
|
||||
|
||||
const { findAllByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
|
||||
|
||||
expect(ayeVoters).toHaveLength(2);
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
|
||||
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
|
||||
expect(ayeVoters[1].getAttribute('data-testid')).toContain(bob);
|
||||
});
|
||||
|
||||
it('no voters when no voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals: DeriveCollectiveProposal[] = [];
|
||||
|
||||
const { findAllByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_ayes'))).rejects.toThrow();
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create bounty modal', () => {
|
||||
it('validates bounty length', async () => {
|
||||
bountiesPage.renderMany({ maximumReasonLength: 5 });
|
||||
|
||||
await bountiesPage.openAddBounty();
|
||||
await bountiesPage.enterBountyTitle('longer than 5');
|
||||
|
||||
await bountiesPage.expectText('Title too long');
|
||||
});
|
||||
|
||||
it('validates balance is enough for bond', async () => {
|
||||
bountiesPage.renderMany(
|
||||
{ bountyDepositBase: new BN(10), dataDepositPerByte: new BN(1) },
|
||||
{ balance: 10 }
|
||||
);
|
||||
|
||||
await bountiesPage.openAddBounty();
|
||||
bountiesPage.expectTextAbsent('Account does not have enough funds.');
|
||||
await bountiesPage.enterBountyTitle('add bytes');
|
||||
|
||||
await bountiesPage.expectText('Account does not have enough funds.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('propose curator modal', () => {
|
||||
beforeEach(async () => {
|
||||
bountiesPage.renderOne(bountyWith({ status: 'Funded', value: 5 }));
|
||||
|
||||
await bountiesPage.openProposeCurator();
|
||||
});
|
||||
|
||||
it('shows an error if fee is greater than bounty value', async () => {
|
||||
await bountiesPage.enterCuratorsFee('6');
|
||||
|
||||
await bountiesPage.expectText("Curator's fee can't be higher than bounty value.");
|
||||
});
|
||||
|
||||
it('disables Assign Curator button if validation fails', async () => {
|
||||
await bountiesPage.enterCuratorsFee('6');
|
||||
|
||||
expect(await bountiesPage.assignCuratorButton()).toHaveClass('isDisabled');
|
||||
});
|
||||
|
||||
it('queues propose extrinsic on submit', async () => {
|
||||
await bountiesPage.enterCuratorsFee('0');
|
||||
bountiesPage.enterProposingAccount(alice);
|
||||
bountiesPage.enterProposedCurator(alice);
|
||||
|
||||
fireEvent.click(await bountiesPage.assignCuratorButton());
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockProposeExtrinsic' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('close bounty modal', () => {
|
||||
it('creates closeBounty proposal', async () => {
|
||||
bountiesPage.renderOne(bountyWith({ status: 'Funded' }));
|
||||
await bountiesPage.openCloseBounty();
|
||||
|
||||
bountiesPage.enterProposingAccount(alice);
|
||||
await bountiesPage.clickButton('Close Bounty');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockProposeExtrinsic' });
|
||||
expect(mockBountyHooks.bountyApi.closeBounty).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
|
||||
it('is not available when close bounty motion already exists', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extra-actions')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reject curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' }) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.openRejectCuratorRole();
|
||||
|
||||
await bountiesPage.clickButton('Reject');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: bob });
|
||||
});
|
||||
|
||||
it('shows options for all roles', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'Active' }) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.openExtraActions();
|
||||
|
||||
await bountiesPage.expectText('Give Up');
|
||||
await bountiesPage.expectText('Slash Curator (Council)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accept curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({
|
||||
fee: balanceOf(20),
|
||||
status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' })
|
||||
});
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
await bountiesPage.openAcceptCuratorRole();
|
||||
|
||||
expect(await bountiesPage.findCuratorsFee()).toEqual('20.0000');
|
||||
expect(await bountiesPage.findCuratorsDeposit()).toEqual('10.0000');
|
||||
|
||||
await bountiesPage.clickButton('Accept Curator Role');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: bob });
|
||||
expect(mockBountyHooks.bountyApi.acceptCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('extend bounty expiry action modal', () => {
|
||||
it('queues extend bounty expiry extrinsic on submit', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
await bountiesPage.openExtendExpiry();
|
||||
|
||||
await bountiesPage.enterExpiryRemark('The bounty extend expiry remark');
|
||||
await bountiesPage.clickButton('Accept');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockExtendExtrinsic' });
|
||||
expect(mockBountyHooks.bountyApi.extendBountyExpiry).toHaveBeenCalledWith(aBountyIndex(0), 'The bounty extend expiry remark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slash curator modal', () => {
|
||||
it('gives up on the Curator role of an Active bounty', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
await bountiesPage.openGiveUpCuratorsRole();
|
||||
|
||||
bountiesPage.enterProposingAccount(alice);
|
||||
await bountiesPage.clickButton('Approve');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockUnassignExtrinsic' });
|
||||
expect(mockBountyHooks.bountyApi.unassignCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
|
||||
it('creates a motion when slashing a PendingPayout bounty', async () => {
|
||||
bountiesPage.renderOne(bountyWith({ status: 'PendingPayout' }));
|
||||
|
||||
await bountiesPage.openSlashCuratorByCouncil();
|
||||
|
||||
bountiesPage.enterProposingAccount(alice);
|
||||
|
||||
await bountiesPage.clickButton('Approve');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockProposeExtrinsic' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('award beneficiary action modal', () => {
|
||||
it('awards the beneficiary ', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.openAwardBeneficiary();
|
||||
bountiesPage.enterBeneficiary(bob);
|
||||
await bountiesPage.clickButton('Approve');
|
||||
|
||||
bountiesPage.expectExtrinsicQueued({ accountId: alice, extrinsic: 'mockAwardExtrinsic' });
|
||||
expect(mockBountyHooks.bountyApi.awardBounty).toHaveBeenCalledWith(aBountyIndex(0), bob);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Show', () => {
|
||||
it('warning when update time is close', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith(
|
||||
{
|
||||
curator: alice,
|
||||
status: 'Active',
|
||||
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() - 1
|
||||
}) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Warning');
|
||||
await bountiesPage.expectText('Close deadline');
|
||||
});
|
||||
|
||||
it('info when waiting for bounty funding', async () => {
|
||||
const bounty = bountyWith({ status: 'Approved' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Info');
|
||||
await bountiesPage.expectText('Waiting for Bounty Funding');
|
||||
});
|
||||
|
||||
it('info when waiting for curator acceptance', async () => {
|
||||
const bounty = bountyWith({ status: 'CuratorProposed' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Info');
|
||||
await bountiesPage.expectText('Waiting for Curator\'s acceptance');
|
||||
});
|
||||
|
||||
it('no warning or info when requirements are not met', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({
|
||||
curator: alice,
|
||||
status: 'Active',
|
||||
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() + 1
|
||||
}) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
await bountiesPage.rendered();
|
||||
|
||||
bountiesPage.expectTextAbsent('Warning');
|
||||
bountiesPage.expectTextAbsent('Close deadline');
|
||||
bountiesPage.expectTextAbsent('Info');
|
||||
bountiesPage.expectTextAbsent('Waiting for Bounty Funding');
|
||||
bountiesPage.expectTextAbsent("Waiting for Curator's acceptance");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,712 +0,0 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import BN from 'bn.js';
|
||||
import React, { Suspense } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import { lightTheme } from '@polkadot/apps/themes';
|
||||
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
|
||||
import { ApiContext } from '@polkadot/react-api';
|
||||
import { ApiProps } from '@polkadot/react-api/types';
|
||||
import i18next from '@polkadot/react-components/i18n';
|
||||
import { QueueProvider } from '@polkadot/react-components/Status/Context';
|
||||
import { QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
|
||||
import { createAugmentedApi } from '@polkadot/test-support/api';
|
||||
import { balanceOf } from '@polkadot/test-support/creation/balance';
|
||||
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
|
||||
import { MemoryStore } from '@polkadot/test-support/keyring';
|
||||
import { TypeRegistry } from '@polkadot/types/create';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { extractTime } from '@polkadot/util';
|
||||
|
||||
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from '../src/BountyInfos';
|
||||
import { alice,
|
||||
bob,
|
||||
defaultBalance,
|
||||
defaultBountyApi,
|
||||
defaultBountyUpdatePeriod,
|
||||
defaultMembers,
|
||||
defaultTreasury } from '../test/hooks/defaults';
|
||||
import { clickButtonWithName } from '../test/utils/clickButtonWithName';
|
||||
import { clickElementWithTestId } from '../test/utils/clickElementWithTestId';
|
||||
import { clickElementWithText } from '../test/utils/clickElementWithText';
|
||||
import Bounties from './Bounties';
|
||||
import { BountyApi } from './hooks';
|
||||
|
||||
const mockMembers = defaultMembers;
|
||||
const mockTreasury = defaultTreasury;
|
||||
let mockBountyApi = defaultBountyApi;
|
||||
let mockBalance = defaultBalance;
|
||||
|
||||
const mockBlockTime = [50, '', extractTime(1)];
|
||||
|
||||
jest.mock('./hooks/useBalance', () => ({
|
||||
useBalance: () => mockBalance
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/useBounties', () => ({
|
||||
useBounties: () => mockBountyApi
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useTreasury', () => ({
|
||||
useTreasury: () => mockTreasury
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useMembers', () => ({
|
||||
useMembers: () => mockMembers
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useBlockTime', () => ({
|
||||
useBlockTime: () => mockBlockTime
|
||||
}));
|
||||
|
||||
function aGenesisHash () {
|
||||
return new TypeRegistry().createType('Hash', POLKADOT_GENESIS);
|
||||
}
|
||||
|
||||
function aProposal (extrinsic: SubmittableExtrinsic<'promise'>, ayes: string[] = [alice], nays: string[] = [bob]) {
|
||||
return {
|
||||
hash: new TypeRegistry().createType('Hash'),
|
||||
proposal: augmentedApi
|
||||
.registry.createType('Proposal', extrinsic),
|
||||
votes: augmentedApi.registry.createType('Votes', {
|
||||
ayes: ayes,
|
||||
index: 0,
|
||||
nays: nays,
|
||||
threshold: 4
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const propose = jest.fn().mockReturnValue('mockProposeExtrinsic');
|
||||
|
||||
let augmentedApi: ApiPromise;
|
||||
let queueExtrinsic: QueueTxExtrinsicAdd;
|
||||
let aBountyStatus: (status: string) => BountyStatus;
|
||||
let aBountyIndex: (index?:number) => BountyIndex;
|
||||
let aBounty: ({ fee, status, value }?: Partial<Bounty>) => Bounty;
|
||||
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
let bountyStatusWith: ({ curator, status, updateDue }: { curator?: string, status?: string, updateDue?: number}) => BountyStatus;
|
||||
|
||||
describe('Bounties', () => {
|
||||
beforeAll(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
|
||||
augmentedApi = createAugmentedApi();
|
||||
({ aBounty, aBountyIndex, aBountyStatus, bountyStatusWith, bountyWith } = new BountyFactory(augmentedApi));
|
||||
});
|
||||
beforeEach(() => {
|
||||
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
|
||||
});
|
||||
|
||||
const renderBounties = (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}) => {
|
||||
mockBountyApi = { ...mockBountyApi, ...bountyApi };
|
||||
mockBalance = balanceOf(balance);
|
||||
const mockApi: ApiProps = {
|
||||
api: {
|
||||
derive: {
|
||||
accounts: { info: () => Promise.resolve(() => { /**/ }) }
|
||||
},
|
||||
genesisHash: aGenesisHash(),
|
||||
query: {},
|
||||
registry: { chainDecimals: [12], chainTokens: ['Unit'] },
|
||||
tx: {
|
||||
council: {
|
||||
propose
|
||||
}
|
||||
}
|
||||
},
|
||||
systemName: 'substrate'
|
||||
} as unknown as ApiProps;
|
||||
|
||||
const queue = {
|
||||
queueExtrinsic
|
||||
} as QueueProps;
|
||||
|
||||
return render(
|
||||
<Suspense fallback='...'>
|
||||
<QueueProvider value={queue}>
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ApiContext.Provider value={mockApi}>
|
||||
<Bounties/>
|
||||
</ApiContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
</QueueProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
function renderOneBounty (bounty: Bounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = aBountyIndex()) {
|
||||
return renderBounties({ bounties: [{ bounty, description, index, proposals }] });
|
||||
}
|
||||
|
||||
it('creates correct bounty with proposal', () => {
|
||||
const bounty = augmentedApi.registry.createType('Bounty', {
|
||||
curatorDeposit: new BN(1),
|
||||
status: 'Funded',
|
||||
value: new BN(10)
|
||||
});
|
||||
|
||||
expect(bounty.curatorDeposit.eq(new BN(1))).toBeTruthy();
|
||||
expect(bounty.status.isFunded).toBeTruthy();
|
||||
expect(bounty.value.eq(new BN(10))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates correct proposal', () => {
|
||||
const proposal = augmentedApi.registry.createType('Proposal', augmentedApi.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1));
|
||||
|
||||
expect(proposal.args[0].eq(new BN(0))).toBeTruthy();
|
||||
expect(proposal.args[1].toString()).toEqual('5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z');
|
||||
expect(proposal.args[2].eq(new BN(1))).toBeTruthy();
|
||||
expect(proposal.method).toEqual('proposeCurator');
|
||||
});
|
||||
|
||||
it('creates correct votes', () => {
|
||||
const votes = augmentedApi.registry.createType('Votes', { ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], index: 0, nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'], threshold: 4 });
|
||||
|
||||
expect(votes.ayes.length).toEqual(1);
|
||||
expect(votes.ayes[0].toString()).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
|
||||
expect(votes.nays.length).toEqual(1);
|
||||
expect(votes.nays[0].toString()).toEqual('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty');
|
||||
expect(votes.index.toNumber()).toEqual(0);
|
||||
expect(votes.threshold.toNumber()).toEqual(4);
|
||||
});
|
||||
|
||||
it('shows empty list when no bounties', async () => {
|
||||
const { findByText } = renderBounties();
|
||||
|
||||
expect(await findByText('No open bounties')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders a bounty', async () => {
|
||||
const { findByText, queryAllByText } = renderOneBounty(aBounty(), [], 'kusama comic book');
|
||||
|
||||
expect(await findByText('kusama comic book')).toBeTruthy();
|
||||
expect(queryAllByText('No open bounties')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders bounties in order from newest to oldest', async () => {
|
||||
const bounty1 = bountyWith({ status: 'Proposed' });
|
||||
const bounty2 = bountyWith({ status: 'Proposed' });
|
||||
const bounty3 = bountyWith({ status: 'Proposed' });
|
||||
|
||||
const { findAllByTestId } = renderBounties({ bounties: [
|
||||
{ bounty: bounty1, description: 'bounty 2', index: aBountyIndex(2), proposals: [] },
|
||||
{ bounty: bounty2, description: 'bounty 1', index: aBountyIndex(1), proposals: [] },
|
||||
{ bounty: bounty3, description: 'bounty 3', index: aBountyIndex(3), proposals: [] }
|
||||
] });
|
||||
|
||||
const descriptions = await findAllByTestId('description');
|
||||
|
||||
expect(descriptions[0].textContent).toEqual('bounty 3');
|
||||
expect(descriptions[1].textContent).toEqual('bounty 2');
|
||||
expect(descriptions[2].textContent).toEqual('bounty 1');
|
||||
});
|
||||
|
||||
describe('create bounty modal', () => {
|
||||
it('validates bounty length', async () => {
|
||||
const { findByTestId, findByText } = renderBounties({ maximumReasonLength: 5 });
|
||||
|
||||
const addBountyButton = await findByText('Add Bounty');
|
||||
|
||||
fireEvent.click(addBountyButton);
|
||||
|
||||
const titleInput = await findByTestId('bounty title');
|
||||
|
||||
fireEvent.change(titleInput, { target: { value: 'longer than 5' } });
|
||||
|
||||
expect(await findByText('Title too long')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('validates balance is enough for bond', async () => {
|
||||
const { findByRole, findByTestId, findByText, queryByText } = renderBounties(
|
||||
{ bountyDepositBase: new BN(10), dataDepositPerByte: new BN(1) },
|
||||
{ balance: 10 }
|
||||
);
|
||||
|
||||
await clickButtonWithName('Add Bounty', findByRole);
|
||||
|
||||
expect(await findByText('Description of the Bounty (to be stored on-chain)')).toBeTruthy(); // wait for load
|
||||
|
||||
expect(queryByText('Account does not have enough funds.')).toBeFalsy();
|
||||
|
||||
const titleInput = await findByTestId('bounty title');
|
||||
|
||||
fireEvent.change(titleInput, { target: { value: 'add bytes' } });
|
||||
|
||||
expect(await findByText('Account does not have enough funds.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('propose curator modal', () => {
|
||||
it('shows an error if fee is greater than bounty value', async () => {
|
||||
const bounty = { status: aBountyStatus('Funded'), value: balanceOf(5) };
|
||||
const { findByTestId, findByText } = renderOneBounty(aBounty(bounty));
|
||||
|
||||
const proposeCuratorButton = await findByText('Propose Curator');
|
||||
|
||||
fireEvent.click(proposeCuratorButton);
|
||||
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();
|
||||
|
||||
const feeInput = await findByTestId("curator's fee");
|
||||
|
||||
fireEvent.change(feeInput, { target: { value: '6' } });
|
||||
|
||||
expect(await findByText("Curator's fee can't be higher than bounty value.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it('disables Assign Curator button if validation fails', async () => {
|
||||
const bounty = { status: aBountyStatus('Funded'), value: balanceOf(5) };
|
||||
const { findByTestId, findByText } = renderOneBounty(aBounty(bounty));
|
||||
|
||||
const proposeCuratorButton = await findByText('Propose Curator');
|
||||
|
||||
fireEvent.click(proposeCuratorButton);
|
||||
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();
|
||||
|
||||
const feeInput = await findByTestId("curator's fee");
|
||||
|
||||
fireEvent.change(feeInput, { target: { value: '6' } });
|
||||
|
||||
const assignCuratorButton = await findByText('Assign curator');
|
||||
|
||||
expect(assignCuratorButton.classList.contains('isDisabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('queues propose extrinsic on submit', async () => {
|
||||
const bounty = { status: aBountyStatus('Funded') };
|
||||
const { findByTestId, findByText, getAllByRole } = renderOneBounty(aBounty(bounty));
|
||||
|
||||
const proposeCuratorButton = await findByText('Propose Curator');
|
||||
|
||||
fireEvent.click(proposeCuratorButton);
|
||||
expect(await findByText('This action will create a Council motion to assign a Curator.')).toBeTruthy();
|
||||
|
||||
const feeInput = await findByTestId("curator's fee");
|
||||
|
||||
fireEvent.change(feeInput, { target: { value: '0' } });
|
||||
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
const proposingCuratorInput = comboboxes[1].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
fireEvent.change(proposingCuratorInput, { target: { value: alice } });
|
||||
|
||||
const assignCuratorButton = await findByText('Assign curator');
|
||||
|
||||
fireEvent.click(assignCuratorButton);
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('close bounty modal', () => {
|
||||
it('creates closeBounty proposal', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const { findByRole, findByTestId, findByText, getByRole } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Close', findByText);
|
||||
|
||||
expect(await findByText('This action will create a Council proposal to close the Bounty.')).toBeTruthy();
|
||||
|
||||
const combobox = getByRole('combobox');
|
||||
const proposingAccountInput = combobox.children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
|
||||
await clickButtonWithName('Close Bounty', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
expect(mockBountyApi.closeBounty).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
|
||||
it('Not available when close bounty motion already exists', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
|
||||
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extra-actions')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reject curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' }) });
|
||||
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Reject Curator', findByText);
|
||||
|
||||
await clickButtonWithName('Reject', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: bob }));
|
||||
});
|
||||
|
||||
it('shows options for all roles', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: bob, status: 'Active' }) });
|
||||
|
||||
const { findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
expect(await findByText('Give Up')).toBeTruthy();
|
||||
expect(await findByText('Slash Curator (Council)')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accept curator modal', () => {
|
||||
it('creates extrinsic', async () => {
|
||||
const bounty = aBounty({ fee: balanceOf(20), status: bountyStatusWith({ curator: bob, status: 'CuratorProposed' }) });
|
||||
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickButtonWithName('Accept', findByRole);
|
||||
|
||||
expect(await findByText('This action will accept your candidacy for the curator of the bounty.')).toBeTruthy();
|
||||
|
||||
const fee = (await findByTestId("curator's fee")).getAttribute('value');
|
||||
|
||||
expect(fee).toEqual('20.0000');
|
||||
|
||||
const deposit = (await findByTestId("curator's deposit")).getAttribute('value');
|
||||
|
||||
expect(deposit).toEqual('10.0000');
|
||||
|
||||
await clickButtonWithName('Accept Curator Role', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: bob }));
|
||||
expect(mockBountyApi.acceptCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('extend bounty expiry action modal', () => {
|
||||
it('queues extend bounty expiry extrinsic on submit', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
|
||||
const { findByRole, findByTestId, findByText } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Extend Expiry', findByText);
|
||||
|
||||
expect(await findByText('This action will extend expiry time of the selected bounty.')).toBeTruthy();
|
||||
|
||||
const remarkInput = await findByTestId('bounty remark');
|
||||
|
||||
fireEvent.change(remarkInput, { target: { value: 'The bounty extend expiry remark' } });
|
||||
|
||||
await clickButtonWithName('Accept', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockExtendExtrinsic' }));
|
||||
expect(mockBountyApi.extendBountyExpiry).toHaveBeenCalledWith(aBountyIndex(0), 'The bounty extend expiry remark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slash curator action modal', () => {
|
||||
it('give up on a Curator function in Active state bounty', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
const { findByRole, findByTestId, findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Give Up', findByText);
|
||||
|
||||
expect(await findByText('This action will unassign you from a curator role.')).toBeTruthy();
|
||||
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
|
||||
await clickButtonWithName('Approve', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockUnassignExtrinsic' }));
|
||||
expect(mockBountyApi.unassignCurator).toHaveBeenCalledWith(aBountyIndex(0));
|
||||
});
|
||||
|
||||
it('for bounty in PendingPayout state', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
|
||||
const { findByRole, findByTestId, findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
await clickElementWithTestId('extra-actions', findByTestId);
|
||||
|
||||
await clickElementWithText('Slash Curator (Council)', findByText);
|
||||
|
||||
expect(await findByText('This action will create a Council motion to slash the Curator.')).toBeTruthy();
|
||||
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: alice } });
|
||||
fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
|
||||
await clickButtonWithName('Approve', findByRole);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockProposeExtrinsic' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('award beneficiary action modal', () => {
|
||||
it('awards the beneficiary ', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({ curator: alice }) });
|
||||
const { findByText, getAllByRole } = renderOneBounty(bounty);
|
||||
|
||||
const awardBeneficiaryButton = await findByText('Award Beneficiary');
|
||||
|
||||
fireEvent.click(awardBeneficiaryButton);
|
||||
expect(await findByText('This action will award the Beneficiary and close the bounty after a delay.')).toBeTruthy();
|
||||
const comboboxes = getAllByRole('combobox');
|
||||
|
||||
const beneficiaryAccountInput = comboboxes[1].children[0];
|
||||
|
||||
fireEvent.change(beneficiaryAccountInput, { target: { value: bob } });
|
||||
fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
|
||||
const acceptButton = await findByText('Approve');
|
||||
|
||||
fireEvent.click(acceptButton);
|
||||
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: alice, extrinsic: 'mockAwardExtrinsic' }));
|
||||
expect(mockBountyApi.awardBounty).toHaveBeenCalledWith(aBountyIndex(0), bob);
|
||||
});
|
||||
});
|
||||
|
||||
describe('status is extended', () => {
|
||||
it('on proposed curator', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Curator under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on bounty approval in proposed status', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Bounty approval under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on parallel bounty approval and bounty close in proposed status', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [
|
||||
aProposal(augmentedApi.tx.bounties.closeBounty(0)),
|
||||
aProposal(augmentedApi.tx.bounties.approveBounty(0))
|
||||
];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Bounty approval under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on close bounty in active status', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Bounty rejection under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on unassign curator in active state', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.unassignCurator(0))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Curator slash under voting')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('on bounty approval in active state', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extendedVotingStatus')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('when bounty is claimable', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const { findByTestId } = renderOneBounty(bounty);
|
||||
|
||||
expect((await findByTestId('extendedActionStatus')).textContent).toEqual('Claimable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Proposed Curator extended status', () => {
|
||||
it('Funded and propose curator motion', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.proposeCurator(0, alice, 1))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Proposed Curator')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Funded and no propose curator motion', async () => {
|
||||
const bounty = bountyWith({ status: 'Funded' });
|
||||
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
await expect(findByText('Proposed Curator')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('CuratorProposed', async () => {
|
||||
const bounty = bountyWith({ status: 'CuratorProposed' });
|
||||
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
await expect(findByText('Proposed Curator')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Voters', () => {
|
||||
it('aye and nay', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
|
||||
const { findAllByTestId } = renderOneBounty(bounty, proposals);
|
||||
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
|
||||
const nayVoters = await findAllByTestId((testId) => testId.startsWith('voters_nays'));
|
||||
|
||||
expect(ayeVoters).toHaveLength(1);
|
||||
expect(nayVoters).toHaveLength(1);
|
||||
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
|
||||
expect(nayVoters[0].getAttribute('data-testid')).toContain(bob);
|
||||
});
|
||||
|
||||
it('multiple aye and no nay', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
|
||||
|
||||
const { findAllByTestId } = renderOneBounty(bounty, proposals);
|
||||
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
|
||||
|
||||
expect(ayeVoters).toHaveLength(2);
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
|
||||
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
|
||||
expect(ayeVoters[1].getAttribute('data-testid')).toContain(bob);
|
||||
});
|
||||
|
||||
it('not displayed when no voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals: DeriveCollectiveProposal[] = [];
|
||||
|
||||
const { findAllByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_ayes'))).rejects.toThrow();
|
||||
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Voting summary', () => {
|
||||
it('is displayed when voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
|
||||
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting results');
|
||||
});
|
||||
|
||||
it('is not displayed when no voting', async () => {
|
||||
const bounty = bountyWith({ status: 'Proposed' });
|
||||
const proposals: DeriveCollectiveProposal[] = [];
|
||||
|
||||
const { findByTestId } = renderOneBounty(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('voting-summary')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Beneficiary description', () => {
|
||||
it('PendingPayout status', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const proposals = [aProposal(augmentedApi.tx.bounties.awardBounty(0, alice))];
|
||||
|
||||
const { findByText } = renderOneBounty(bounty, proposals);
|
||||
|
||||
expect(await findByText('Beneficiary')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Other status', async () => {
|
||||
const bounty = bountyWith({ status: 'Active' });
|
||||
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
await expect(findByText('Beneficiary')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Show', () => {
|
||||
it('warning when update time is close', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith(
|
||||
{
|
||||
curator: alice,
|
||||
status: 'Active',
|
||||
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() - 1
|
||||
}) });
|
||||
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
expect(await findByText('Warning')).toBeTruthy();
|
||||
expect(await findByText('Close deadline')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('info when waiting for bounty funding', async () => {
|
||||
const bounty = bountyWith({ status: 'Approved' });
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
expect(await findByText('Info')).toBeTruthy();
|
||||
expect(await findByText('Waiting for Bounty Funding')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('info when waiting for curator acceptance', async () => {
|
||||
const bounty = bountyWith({ status: 'CuratorProposed' });
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
expect(await findByText('Info')).toBeTruthy();
|
||||
expect(await findByText("Waiting for Curator's acceptance")).toBeTruthy();
|
||||
});
|
||||
|
||||
it('no warning or info when requirements are not met', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({
|
||||
curator: alice,
|
||||
status: 'Active',
|
||||
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() + 1
|
||||
}) });
|
||||
|
||||
const { findByText } = renderOneBounty(bounty);
|
||||
|
||||
await expect(findByText('Warning')).rejects.toThrow();
|
||||
await expect(findByText('Close deadline')).rejects.toThrow();
|
||||
|
||||
await expect(findByText('Info')).rejects.toThrow();
|
||||
await expect(findByText('Waiting for Bounty Funding')).rejects.toThrow();
|
||||
await expect(findByText("Waiting for Curator's acceptance")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -37,7 +37,8 @@ function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', pr
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='bountyStatus'>
|
||||
<div className='bountyStatus'
|
||||
data-testid={'bountyStatus'}>
|
||||
{insertSpaceBeforeCapitalLetter(bountyStatus)}
|
||||
<LabelHelp
|
||||
help={statusHelpMessages.current[bountyStatus]}
|
||||
|
||||
@ -31,17 +31,9 @@ export const defaultBountyApi: BountyApi = {
|
||||
unassignCurator: jest.fn().mockReturnValue('mockUnassignExtrinsic')
|
||||
};
|
||||
|
||||
export const defaultBalance = balanceOf(1);
|
||||
const defaultBalance = balanceOf(1);
|
||||
|
||||
export const defaultTreasury = {
|
||||
burn: new BN(1),
|
||||
spendPeriod: new BN(0),
|
||||
value: balanceOf(1)
|
||||
export const mockBountyHooks = {
|
||||
balance: defaultBalance,
|
||||
bountyApi: defaultBountyApi
|
||||
};
|
||||
|
||||
export const alice = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
export const bob = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
|
||||
export const ferdie = '5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL';
|
||||
export const defaultCurator = '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM';
|
||||
|
||||
export const defaultMembers = { isMember: true, members: [alice, bob, ferdie] };
|
||||
|
||||
307
packages/page-bounties/test/pages/bountiesPage.tsx
Normal file
307
packages/page-bounties/test/pages/bountiesPage.tsx
Normal file
@ -0,0 +1,307 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import Bounties from '@polkadot/app-bounties/Bounties';
|
||||
import { BountyApi } from '@polkadot/app-bounties/hooks';
|
||||
import { lightTheme } from '@polkadot/apps/themes';
|
||||
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
|
||||
import { ApiContext } from '@polkadot/react-api';
|
||||
import { ApiProps } from '@polkadot/react-api/types';
|
||||
import { QueueProvider } from '@polkadot/react-components/Status/Context';
|
||||
import { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
|
||||
import { balanceOf } from '@polkadot/test-support/creation/balance';
|
||||
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
|
||||
import { TypeRegistry } from '@polkadot/types/create';
|
||||
import { Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import { mockBountyHooks } from '../hooks/defaults';
|
||||
import { clickButtonWithName } from '../utils/clickButtonWithName';
|
||||
import { clickElementWithTestId } from '../utils/clickElementWithTestId';
|
||||
import { clickElementWithText } from '../utils/clickElementWithText';
|
||||
|
||||
function aGenesisHash () {
|
||||
return new TypeRegistry().createType('Hash', POLKADOT_GENESIS);
|
||||
}
|
||||
|
||||
type FindOne = (match: string) => Promise<HTMLElement>;
|
||||
type FindManyWithMatcher = (match: string | ((match: string)=> boolean)) => Promise<HTMLElement[]>
|
||||
type GetMany = (match: string) => HTMLElement[];
|
||||
|
||||
class NotYetRendered extends Error {
|
||||
|
||||
}
|
||||
|
||||
let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
|
||||
const propose = jest.fn().mockReturnValue('mockProposeExtrinsic');
|
||||
|
||||
interface RenderedBountiesPage {
|
||||
findAllByTestId: FindManyWithMatcher;
|
||||
findByText: FindOne,
|
||||
findByRole: FindOne,
|
||||
findByTestId: FindOne,
|
||||
getAllByRole: GetMany,
|
||||
queryAllByText: GetMany
|
||||
}
|
||||
|
||||
export class BountiesPage {
|
||||
aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
|
||||
aBountyIndex: (index?:number) => BountyIndex;
|
||||
aBountyStatus: (status: string) => BountyStatus;
|
||||
bountyStatusWith: ({ curator, status }: { curator?: string, status?: string, }) => BountyStatus;
|
||||
bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
|
||||
|
||||
findByRole?: FindOne;
|
||||
findByText?: FindOne;
|
||||
findByTestId?: FindOne;
|
||||
getAllByRole?: GetMany;
|
||||
findAllByTestId?: FindManyWithMatcher;
|
||||
queryAllByText?: GetMany;
|
||||
|
||||
constructor (api: ApiPromise) {
|
||||
({ aBounty: this.aBounty, aBountyIndex: this.aBountyIndex, aBountyStatus: this.aBountyStatus, bountyStatusWith: this.bountyStatusWith, bountyWith: this.bountyWith } = new BountyFactory(api));
|
||||
}
|
||||
|
||||
renderOne (bounty: Bounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = this.aBountyIndex()): RenderedBountiesPage {
|
||||
return this.renderMany({ bounties: [{ bounty, description, index, proposals }] });
|
||||
}
|
||||
|
||||
renderMany (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}): RenderedBountiesPage {
|
||||
const { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText } = this.renderBounties(bountyApi, { balance });
|
||||
|
||||
this.findByRole = findByRole;
|
||||
this.findByText = findByText;
|
||||
this.findByTestId = findByTestId;
|
||||
this.getAllByRole = getAllByRole;
|
||||
this.findAllByTestId = findAllByTestId;
|
||||
this.queryAllByText = queryAllByText;
|
||||
|
||||
return { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText };
|
||||
}
|
||||
|
||||
private renderBounties (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}) {
|
||||
mockBountyHooks.bountyApi = { ...mockBountyHooks.bountyApi, ...bountyApi };
|
||||
mockBountyHooks.balance = balanceOf(balance);
|
||||
const mockApi: ApiProps = {
|
||||
api: {
|
||||
derive: {
|
||||
accounts: {
|
||||
info: () => Promise.resolve(() => { /**/
|
||||
})
|
||||
}
|
||||
},
|
||||
genesisHash: aGenesisHash(),
|
||||
query: {},
|
||||
registry: { chainDecimals: [12], chainTokens: ['Unit'] },
|
||||
tx: {
|
||||
council: {
|
||||
propose
|
||||
}
|
||||
}
|
||||
},
|
||||
systemName: 'substrate'
|
||||
} as unknown as ApiProps;
|
||||
|
||||
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
|
||||
const queue = {
|
||||
queueExtrinsic
|
||||
} as QueueProps;
|
||||
|
||||
return render(
|
||||
<Suspense fallback='...'>
|
||||
<QueueProvider value={queue}>
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ApiContext.Provider value={mockApi}>
|
||||
<Bounties/>
|
||||
</ApiContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
</QueueProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
private assertRendered (): asserts this is RenderedBountiesPage {
|
||||
if (this.findByText === undefined) {
|
||||
throw new NotYetRendered();
|
||||
}
|
||||
}
|
||||
|
||||
async openProposeCurator (): Promise<void> {
|
||||
this.assertRendered();
|
||||
const proposeCuratorButton = await this.findByText('Propose Curator');
|
||||
|
||||
fireEvent.click(proposeCuratorButton);
|
||||
await this.expectText('This action will create a Council motion to assign a Curator.');
|
||||
}
|
||||
|
||||
async enterCuratorsFee (fee: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
const feeInput = await this.findByTestId("curator's fee");
|
||||
|
||||
fireEvent.change(feeInput, { target: { value: fee } });
|
||||
}
|
||||
|
||||
async expectText (expected: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
expect(await this.findByText(expected)).toBeTruthy();
|
||||
}
|
||||
|
||||
async assignCuratorButton (): Promise<HTMLElement> {
|
||||
this.assertRendered();
|
||||
|
||||
return this.findByText('Assign curator');
|
||||
}
|
||||
|
||||
enterProposingAccount (account: string): void {
|
||||
this.assertRendered();
|
||||
const comboboxes = this.getAllByRole('combobox');
|
||||
|
||||
const proposingAccountInput = comboboxes[0].children[0];
|
||||
|
||||
fireEvent.change(proposingAccountInput, { target: { value: account } });
|
||||
fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
}
|
||||
|
||||
enterProposedCurator (curator: string): void {
|
||||
this.assertRendered();
|
||||
const comboboxes = this.getAllByRole('combobox');
|
||||
|
||||
const proposedCuratorInput = comboboxes[1].children[0];
|
||||
|
||||
fireEvent.change(proposedCuratorInput, { target: { value: curator } });
|
||||
fireEvent.keyDown(proposedCuratorInput, { code: 'Enter', key: 'Enter' });
|
||||
}
|
||||
|
||||
expectExtrinsicQueued (extrinsicPart: { accountId: string; extrinsic?: string }): void {
|
||||
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining(extrinsicPart));
|
||||
}
|
||||
|
||||
expectTextAbsent (text: string): void {
|
||||
this.assertRendered();
|
||||
expect(this.queryAllByText(text)).toHaveLength(0);
|
||||
}
|
||||
|
||||
async findAllDescriptions (): Promise<string[]> {
|
||||
this.assertRendered();
|
||||
const descriptions = await this.findAllByTestId('description');
|
||||
|
||||
return descriptions.map((d) => d.textContent || '');
|
||||
}
|
||||
|
||||
async rendered (): Promise<void> {
|
||||
this.assertRendered();
|
||||
await this.findByTestId('bountyStatus');
|
||||
}
|
||||
|
||||
async openAddBounty (): Promise<void> {
|
||||
this.assertRendered();
|
||||
await clickButtonWithName('Add Bounty', this.findByRole);
|
||||
await this.expectText('This account will propose the bounty. Bond amount will be reserved on its balance.');
|
||||
}
|
||||
|
||||
async enterBountyTitle (title: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
const titleInput = await this.findByTestId('bounty title');
|
||||
|
||||
fireEvent.change(titleInput, { target: { value: title } });
|
||||
}
|
||||
|
||||
async openCloseBounty (): Promise<void> {
|
||||
this.assertRendered();
|
||||
await this.openExtraActions();
|
||||
|
||||
await clickElementWithText('Close', this.findByText);
|
||||
|
||||
await this.expectText('This action will create a Council proposal to close the Bounty.');
|
||||
}
|
||||
|
||||
async clickButton (buttonName: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
await clickButtonWithName(buttonName, this.findByRole);
|
||||
}
|
||||
|
||||
async clickButtonByTestId (buttonName: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
await clickElementWithTestId(buttonName, this.findByTestId);
|
||||
}
|
||||
|
||||
async clickButtonByText (buttonName: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
await clickElementWithText(buttonName, this.findByText);
|
||||
}
|
||||
|
||||
async openRejectCuratorRole (): Promise<void> {
|
||||
await this.openExtraActions();
|
||||
await this.clickButtonByText('Reject Curator');
|
||||
await this.expectText('This action will reject your candidacy for the curator of the bounty.');
|
||||
}
|
||||
|
||||
async openExtraActions (): Promise<void> {
|
||||
await this.clickButtonByTestId('extra-actions');
|
||||
}
|
||||
|
||||
async openAcceptCuratorRole (): Promise<void> {
|
||||
await this.clickButton('Accept');
|
||||
await this.expectText('This action will accept your candidacy for the curator of the bounty.');
|
||||
}
|
||||
|
||||
async findCuratorsFee (): Promise<string> {
|
||||
this.assertRendered();
|
||||
|
||||
return (await this.findByTestId("curator's fee")).getAttribute('value') || '';
|
||||
}
|
||||
|
||||
async findCuratorsDeposit (): Promise<string> {
|
||||
this.assertRendered();
|
||||
|
||||
return (await this.findByTestId("curator's deposit")).getAttribute('value') || '';
|
||||
}
|
||||
|
||||
async openExtendExpiry (): Promise<void> {
|
||||
await this.openExtraActions();
|
||||
await this.clickButtonByText('Extend Expiry');
|
||||
await this.expectText('This action will extend expiry time of the selected bounty.');
|
||||
}
|
||||
|
||||
async enterExpiryRemark (remark: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
const remarkInput = await this.findByTestId('bounty remark');
|
||||
|
||||
fireEvent.change(remarkInput, { target: { value: remark } });
|
||||
}
|
||||
|
||||
async openGiveUpCuratorsRole (): Promise<void> {
|
||||
await this.openExtraActions();
|
||||
await this.clickButtonByText('Give Up');
|
||||
await this.expectText('This action will unassign you from a curator role.');
|
||||
}
|
||||
|
||||
async openSlashCuratorByCouncil (): Promise<void> {
|
||||
await this.openExtraActions();
|
||||
await this.clickButtonByText('Slash Curator (Council)');
|
||||
await this.expectText('This action will create a Council motion to slash the Curator.');
|
||||
}
|
||||
|
||||
async openAwardBeneficiary (): Promise<void> {
|
||||
await this.clickButton('Award Beneficiary');
|
||||
await this.expectText('This action will award the Beneficiary and close the bounty after a delay.');
|
||||
}
|
||||
|
||||
enterBeneficiary (beneficiary: string): void {
|
||||
this.assertRendered();
|
||||
const comboboxes = this.getAllByRole('combobox');
|
||||
|
||||
const beneficiaryAccountInput = comboboxes[1].children[0];
|
||||
|
||||
fireEvent.change(beneficiaryAccountInput, { target: { value: beneficiary } });
|
||||
fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
"testcontainers": "^6.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"ts-node": "^9.1.1",
|
||||
"tsconfig-paths": "^3.9.0"
|
||||
}
|
||||
|
||||
15
packages/test-support/src/creation/hashes.ts
Normal file
15
packages/test-support/src/creation/hashes.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright 2017-2021 @polkadot/test-support authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
|
||||
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
|
||||
import { TypeRegistry } from '@polkadot/types/create';
|
||||
|
||||
export function aGenesisHash (): Hash {
|
||||
return new TypeRegistry().createType('Hash', POLKADOT_GENESIS);
|
||||
}
|
||||
|
||||
export function aHash (): Hash {
|
||||
return new TypeRegistry().createType('Hash');
|
||||
}
|
||||
12
packages/test-support/src/creation/treasury/defaults.ts
Normal file
12
packages/test-support/src/creation/treasury/defaults.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2017-2021 @polkadot/test-support authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { balanceOf } from '../balance';
|
||||
|
||||
export const defaultTreasury = {
|
||||
burn: new BN(1),
|
||||
spendPeriod: new BN(0),
|
||||
value: balanceOf(1)
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
// Copyright 2017-2020 @polkadot/api authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import { aHash } from '@polkadot/test-support/creation/hashes';
|
||||
import { alice, bob } from '@polkadot/test-support/keyring/addresses';
|
||||
|
||||
export interface ProposalFactory {
|
||||
aProposal: (extrinsic: SubmittableExtrinsic<'promise'>, ayes?: string[], nays?: string[]) => DeriveCollectiveProposal
|
||||
}
|
||||
|
||||
export function proposalFactory (api: ApiPromise): ProposalFactory {
|
||||
const registry = api.registry;
|
||||
|
||||
return {
|
||||
aProposal: (extrinsic, ayes = [alice], nays = [bob]) => ({
|
||||
hash: aHash(),
|
||||
proposal: registry.createType('Proposal', extrinsic),
|
||||
votes: registry.createType('Votes', {
|
||||
ayes: ayes,
|
||||
index: 0,
|
||||
nays: nays,
|
||||
threshold: 4
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
12
packages/test-support/src/hooks/mockHooks.ts
Normal file
12
packages/test-support/src/hooks/mockHooks.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2017-2021 @polkadot/test-support authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { defaultTreasury } from '@polkadot/test-support/creation/treasury/defaults';
|
||||
import { defaultMembers } from '@polkadot/test-support/keyring/addresses';
|
||||
import { extractTime } from '@polkadot/util';
|
||||
|
||||
export const mockHooks = {
|
||||
blockTime: [50, '', extractTime(1)],
|
||||
members: defaultMembers,
|
||||
treasury: defaultTreasury
|
||||
};
|
||||
7
packages/test-support/src/keyring/addresses.ts
Normal file
7
packages/test-support/src/keyring/addresses.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/test-support authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const alice = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
export const bob = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
|
||||
export const ferdie = '5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL';
|
||||
export const defaultMembers = { isMember: true, members: [alice, bob, ferdie] };
|
||||
58
yarn.lock
58
yarn.lock
@ -1307,7 +1307,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4":
|
||||
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
|
||||
version: 7.12.5
|
||||
resolution: "@babel/runtime@npm:7.12.5"
|
||||
dependencies:
|
||||
@ -2633,6 +2633,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/test-support@workspace:packages/test-support"
|
||||
dependencies:
|
||||
"@testing-library/jest-dom": ^5.11.9
|
||||
"@testing-library/react": ^11.2.3
|
||||
testcontainers: ^6.4.2
|
||||
ts-node: ^9.1.1
|
||||
@ -3062,6 +3063,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/jest-dom@npm:^5.11.9":
|
||||
version: 5.11.9
|
||||
resolution: "@testing-library/jest-dom@npm:5.11.9"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.9.2
|
||||
"@types/testing-library__jest-dom": ^5.9.1
|
||||
aria-query: ^4.2.2
|
||||
chalk: ^3.0.0
|
||||
css: ^3.0.0
|
||||
css.escape: ^1.5.1
|
||||
lodash: ^4.17.15
|
||||
redent: ^3.0.0
|
||||
checksum: c285c044edf42b9c4daffbeddd5489564af1c9d625f0ef3781e2e9864688239ccc2521badec9147fea66685c60b736c922e35ea69011aa577eabc156c0e1f5b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/react@npm:^11.2.3":
|
||||
version: 11.2.3
|
||||
resolution: "@testing-library/react@npm:11.2.3"
|
||||
@ -3345,7 +3362,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jest@npm:^26.0.20":
|
||||
"@types/jest@npm:*, @types/jest@npm:^26.0.20":
|
||||
version: 26.0.20
|
||||
resolution: "@types/jest@npm:26.0.20"
|
||||
dependencies:
|
||||
@ -3633,6 +3650,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/testing-library__jest-dom@npm:^5.9.1":
|
||||
version: 5.9.5
|
||||
resolution: "@types/testing-library__jest-dom@npm:5.9.5"
|
||||
dependencies:
|
||||
"@types/jest": "*"
|
||||
checksum: 32aa324a83e67fd81eb7f0ba3e4c86406db86c1e5361b6e8091d224a1fefbfcb4e493039db3ce3faf8afa133a4f68cc7c42bcdbcae1dd91b13d741a040791038
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tmp@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@types/tmp@npm:0.2.0"
|
||||
@ -6628,6 +6654,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css.escape@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "css.escape@npm:1.5.1"
|
||||
checksum: 44fe5e93fee46fe60dbd0cdd078b14ef75697ee93519a7157f976b655463dd66eba598b0df16c16a897ac884c97845d2a3819cb8d370cbf91bc59bb557ebe826
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "css@npm:3.0.0"
|
||||
dependencies:
|
||||
inherits: ^2.0.4
|
||||
source-map: ^0.6.1
|
||||
source-map-resolve: ^0.6.0
|
||||
checksum: a62cc8350fa1f6805a81447dc4a1df30f37316bcb671e4669531b5ddf24a8dd7024324ce321a8ec0b1dd48baafb38781ae4b0d80fa7c7f5adab5f46b8b9e29c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cssesc@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "cssesc@npm:3.0.0"
|
||||
@ -16614,6 +16658,16 @@ fsevents@~2.1.2:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"source-map-resolve@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "source-map-resolve@npm:0.6.0"
|
||||
dependencies:
|
||||
atob: ^2.1.2
|
||||
decode-uri-component: ^0.2.0
|
||||
checksum: 9bbd2656937ce422f1526026638a91dacba0822f1ab954ac34728c9b4f2960dbfc22a8463af9fbf74f28f3722263444df79b2b5ffa07de07dbcda2ec5a8c7513
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.17, source-map-support@npm:^0.5.19, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.19":
|
||||
version: 0.5.19
|
||||
resolution: "source-map-support@npm:0.5.19"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user