Bounties - refactor tests (#4589)

This commit is contained in:
Krzysztof Jelski 2021-02-04 09:16:23 +01:00 committed by GitHub
parent 54567442c1
commit cb954bc628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 955 additions and 727 deletions

View File

@ -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
View 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;
} };

View 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");
});
});
});

View File

@ -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();
});
});
});

View File

@ -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]}

View File

@ -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] };

View 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' });
}
}

View File

@ -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"
}

View 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');
}

View 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)
};

View File

@ -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
})
})
};
}

View 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
};

View 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] };

View File

@ -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"