// Copyright 2017-2026 @pezkuwi/app-bounties authors & contributors // SPDX-License-Identifier: Apache-2.0 /* global jest, expect */ import type { RenderResult } from '@testing-library/react'; import type { ApiPromise } from '@pezkuwi/api'; import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types'; import type { ApiProps } from '@pezkuwi/react-api/types'; import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@pezkuwi/react-components/Status/types'; import type { BountyIndex } from '@pezkuwi/types/interfaces'; import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup'; import type { BountyApi } from '../../src/hooks/index.js'; import { fireEvent, render, within } from '@testing-library/react'; import React, { Suspense } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config'; import { lightTheme } from '@pezkuwi/react-components'; import { KeyringCtxRoot } from '@pezkuwi/react-hooks'; import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api'; import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue'; import { balanceOf } from '@pezkuwi/test-support/creation/balance'; import { BountyFactory } from '@pezkuwi/test-support/creation/bounties'; import { TypeRegistry } from '@pezkuwi/types/create'; import Bounties from '../../src/Bounties.js'; import { mockBountyHooks } from '../hooks/defaults.js'; import { clickButtonWithName } from '../utils/clickButtonWithName.js'; import { clickElementWithTestId } from '../utils/clickElementWithTestId.js'; import { clickElementWithText } from '../utils/clickElementWithText.js'; function aGenesisHash () { return new TypeRegistry().createType('Hash', PEZKUWI_GENESIS); } type FindOne = (match: string) => Promise; type FindManyWithMatcher = (match: string | ((match: string) => boolean)) => Promise type GetMany = (match: string) => HTMLElement[]; class NotYetRendered extends Error { } let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void; const propose = jest.fn(() => 'mockProposeExtrinsic'); interface RenderedBountiesPage { findAllByTestId: FindManyWithMatcher; findByText: FindOne; findByRole: FindOne; findByTestId: FindOne; getAllByRole: GetMany; queryAllByText: GetMany; } export class BountiesPage { aBounty: ({ status, value }?: Partial) => PezpalletBountiesBounty; aBountyIndex: (index?: number) => BountyIndex; aBountyStatus: (status: string) => PezpalletBountiesBountyStatus; bountyStatusWith: ({ curator, status }: { curator?: string, status?: string, }) => PezpalletBountiesBountyStatus; bountyWith: ({ status, value }: { status?: string, value?: number }) => PezpalletBountiesBounty; findByRole?: FindOne; findByText?: FindOne; findByTestId?: FindOne; getAllByRole?: GetMany; findAllByTestId?: FindManyWithMatcher; queryAllByText?: GetMany; renderResult?: RenderResult; constructor (api: ApiPromise) { ({ aBounty: this.aBounty, aBountyIndex: this.aBountyIndex, aBountyStatus: this.aBountyStatus, bountyStatusWith: this.bountyStatusWith, bountyWith: this.bountyWith } = new BountyFactory(api as any)); } renderOne (bounty: PezpalletBountiesBounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = this.aBountyIndex()): RenderedBountiesPage { return this.renderMany({ bounties: [{ bounty, description, index, proposals }] }); } renderMany (bountyApi: Partial = {}, { balance = 1 } = {}): RenderedBountiesPage { const renderResult = this.renderBounties(bountyApi, { balance }); const { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText } = renderResult; this.findByRole = findByRole; this.findByText = findByText; this.findByTestId = findByTestId; this.getAllByRole = getAllByRole; this.findAllByTestId = findAllByTestId; this.queryAllByText = queryAllByText; this.renderResult = renderResult; return { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText }; } private renderBounties (bountyApi: Partial = {}, { 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 } } }, isApiConnected: true, isApiInitialized: true, isApiReady: true, isEthereum: false, systemName: 'bizinikiwi' } as unknown as ApiProps; queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd; const queue = { queueExtrinsic } as QueueProps; return render( <>
); } private assertRendered (): asserts this is RenderedBountiesPage { if (this.findByText === undefined) { throw new NotYetRendered(); } } async openProposeCurator (): Promise { this.assertRendered(); const proposeCuratorButton = await this.findByText('Propose curator'); fireEvent.click(proposeCuratorButton); // await this.expectText('This action will create a Council motion to propose a Curator for the Bounty.'); } async enterCuratorsFee (fee: string): Promise { this.assertRendered(); const feeInput = await this.findByTestId("curator's fee"); fireEvent.change(feeInput, { target: { value: fee } }); } async expectText (expected: string): Promise { this.assertRendered(); expect(await this.findByText(expected)).toBeTruthy(); } async assignCuratorButton (): Promise { this.assertRendered(); const proposeCuratorModal = await this.findByTestId('propose-curator-modal'); return await within(proposeCuratorModal).findByText('Propose 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 { this.assertRendered(); const descriptions = await this.findAllByTestId('description'); return descriptions.map((d) => d.textContent || ''); } async rendered (): Promise { this.assertRendered(); await this.findByTestId('bountyStatus'); } async openAddBounty (): Promise { 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 { this.assertRendered(); const titleInput = await this.findByTestId('bounty title'); fireEvent.change(titleInput, { target: { value: title } }); } async openCloseBounty (): Promise { 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 { this.assertRendered(); await clickButtonWithName(buttonName, this.findByRole); } async clickButtonByTestId (buttonName: string): Promise { this.assertRendered(); await clickElementWithTestId(buttonName, this.findByTestId); } async clickButtonByText (buttonName: string): Promise { this.assertRendered(); await clickElementWithText(buttonName, this.findByText); } async openRejectCuratorRole (): Promise { 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 { await this.clickButtonByTestId('popup-open'); } async openAcceptCuratorRole (): Promise { await this.clickButton('Accept'); // await this.expectText('This action will accept your candidacy for the curator of the bounty.'); } async findCuratorsFee (): Promise { this.assertRendered(); return (await this.findByTestId("curator's fee")).getAttribute('value') || ''; } async findCuratorsDeposit (): Promise { this.assertRendered(); return (await this.findByTestId("curator's deposit")).getAttribute('value') || ''; } async openExtendExpiry (): Promise { 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 { this.assertRendered(); const remarkInput = await this.findByTestId('bounty remark'); fireEvent.change(remarkInput, { target: { value: remark } }); } async openGiveUpCuratorsRole (): Promise { await this.openExtraActions(); await this.clickButtonByText('Give up'); // await this.expectText('This action will unassign you from the curator role.'); } async openSlashCuratorByCouncil (): Promise { 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 { await this.clickButton('Reward implementer'); // await this.expectText('This action will reward the Beneficiary and close the bounty after a delay period.'); } 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' }); } async expectVotingDescription (description: string): Promise { this.assertRendered(); const votingInfo = await this.findByTestId('voting-description'); const icon = await within(votingInfo).findByTestId('question-circle'); fireEvent.mouseEnter(icon); expect(await this.findByText(description)).toBeVisible(); } }