feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
View File
View File
+32
View File
@@ -0,0 +1,32 @@
{
"bugs": "https://github.com/pezkuwichain/pezkuwi-apps/issues",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/pezkuwichain/pezkuwi-apps/tree/master/packages/test-support#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/test-support",
"private": true,
"repository": {
"directory": "packages/test-support",
"type": "git",
"url": "https://github.com/pezkuwichain/pezkuwi-apps.git"
},
"sideEffects": false,
"type": "module",
"version": "0.168.2-4-x",
"dependencies": {
"@testing-library/react": "^14.1.2",
"testcontainers": "^10.4.0"
},
"devDependencies": {
"@pezkuwi/types-support": "16.5.2",
"@testing-library/jest-dom": "^5.17.0",
"tsconfig-paths": "^4.2.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"react-is": "*"
}
}
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { createApi } from '@pezkuwi/test-support/api';
import { aliceSigner } from '@pezkuwi/test-support/keyring';
import { multiAcceptCurator, multiApproveBounty, multiAwardBounty, multiClaimBounty, multiProposeBounty, multiProposeCurator, multiWaitForBountyFunded, multiWaitForClaim } from './lib/multiFunctions.js';
(async () => {
const api = await createApi(9944);
const indexes = await multiProposeBounty(api, 6, aliceSigner());
indexes.pop();
await multiApproveBounty(api, indexes, aliceSigner());
await multiWaitForBountyFunded(api, indexes);
indexes.pop();
await multiProposeCurator(api, indexes, aliceSigner());
indexes.pop();
await multiAcceptCurator(api, indexes, aliceSigner());
indexes.pop();
await multiAwardBounty(api, indexes, aliceSigner());
await multiWaitForClaim(api, indexes);
indexes.pop();
await multiClaimBounty(api, indexes, aliceSigner());
await api.disconnect();
})().catch((err) => console.error(err));
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { DeriveBounty } from '@pezkuwi/api-derive/types';
import type { WaitOptions } from '@pezkuwi/test-support/types';
import { waitFor } from '@pezkuwi/test-support/utils';
type bStatus = 'isFunded' | 'isActive';
async function getBounty (api: ApiPromise, bountyIndex: number): Promise<DeriveBounty> {
const bounties = await api.derive.bounties.bounties();
const bounty = bounties.find((bounty) => bounty.index.toNumber() === bountyIndex);
if (!bounty) {
throw new Error('Unable to find bounty');
}
return bounty;
}
export async function waitForBountyState (api: ApiPromise, expectedState: bStatus, index: number, { interval = 500,
timeout = 10000 } = {}): Promise<boolean> {
return waitFor(async () => {
const bounty = await getBounty(api, index);
return bounty.bounty.status[expectedState];
}, { interval, timeout });
}
export async function waitForClaim (api: ApiPromise, index: number, { interval = 500, timeout = 10000 }: WaitOptions): Promise<boolean> {
return waitFor(async () => {
const bounty = await getBounty(api, index);
const unlockAt = bounty.bounty.status.asPendingPayout.unlockAt;
const bestNumber = await api.derive.chain.bestNumber();
return unlockAt.lt(bestNumber);
}, { interval, timeout });
}
@@ -0,0 +1,45 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { BN } from '@pezkuwi/util';
import { execute } from '@pezkuwi/test-support/transaction';
import { acceptMotion, fillTreasury, getMotion, proposeMotion } from './helpers.js';
export async function acceptCurator (api: ApiPromise, id: number, signer: KeyringPair): Promise<void> {
await execute(api.tx.bounties.acceptCurator(id), signer);
}
export async function awardBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise<void> {
await execute(api.tx.bounties.awardBounty(index, signer.address), signer);
}
export async function claimBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise<void> {
await execute(api.tx.bounties.claimBounty(index), signer);
}
export async function proposeBounty (api: ApiPromise, value: BN, title: string, signer: KeyringPair): Promise<number> {
await execute(api.tx.bounties.proposeBounty(value, title), signer);
const index = await api.query.bounties.bountyCount();
return index.toNumber() - 1;
}
export async function proposeCurator (api: ApiPromise, index: number, signer: KeyringPair): Promise<void> {
await proposeMotion(api, api.tx.bounties.proposeCurator(index, signer.address, 10), signer);
const bountyProposal = await getMotion(api, index);
await acceptMotion(api, bountyProposal.hash, bountyProposal.votes?.index.toNumber() ?? 0);
}
export async function approveBounty (api: ApiPromise, index: number, signer: KeyringPair): Promise<void> {
await proposeMotion(api, api.tx.bounties.approveBounty(index), signer);
const bountyProposal = await getMotion(api, index);
await acceptMotion(api, bountyProposal.hash, bountyProposal.votes?.index.toNumber() ?? 0);
await fillTreasury(api, signer);
}
@@ -0,0 +1,10 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { BN } from '@pezkuwi/util';
export const TREASURY_ADDRESS = '13UVJyLnbVp9RBZYFwFGyDvVd1y27Tt8tkntv6Q7JVPhFsTB';
export const FUNDING_TIME = 150000;
export const PAYOUT_TIME = 150000;
export const WEIGHT_BOUND = new BN('10000000000');
export const LENGTH_BOUND = 100000;
@@ -0,0 +1,101 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { Hash } from '@pezkuwi/types/interfaces';
import { charlieSigner, daveSigner, eveSigner, ferdieSigner } from '@pezkuwi/test-support/keyring';
import { execute } from '@pezkuwi/test-support/transaction';
import { BN } from '@pezkuwi/util';
import { LENGTH_BOUND, TREASURY_ADDRESS, WEIGHT_BOUND } from './constants.js';
export async function acceptMotion (api: ApiPromise, hash: Hash, index: number): Promise<void> {
const charlieVote = execute(api.tx.council.vote(hash, index, true), charlieSigner());
const daveVote = execute(api.tx.council.vote(hash, index, true), daveSigner());
const eveVote = execute(api.tx.council.vote(hash, index, true), eveSigner());
const ferdieVote = execute(api.tx.council.vote(hash, index, true), ferdieSigner());
await Promise.all([charlieVote, daveVote, eveVote, ferdieVote]);
await execute(api.tx.council.close(hash, index, { refTime: WEIGHT_BOUND }, LENGTH_BOUND), charlieSigner());
}
export async function fillTreasury (api: ApiPromise, signer: KeyringPair): Promise<void> {
await execute((api.tx.balances.transferAllowDeath || api.tx.balances.transfer)(TREASURY_ADDRESS, new BN('50000000000000000')), signer);
}
export async function proposeMotion (api: ApiPromise, submittableExtrinsic: SubmittableExtrinsic<'promise'>, signer: KeyringPair): Promise<void> {
await execute(api.tx.council.propose(4, submittableExtrinsic, LENGTH_BOUND), signer);
}
export async function getMotion (api: ApiPromise, index: number): Promise<DeriveCollectiveProposal> {
const bounties = await api.derive.bounties.bounties();
const bountyProposals = bounties.find((bounty) => (bounty.index.toNumber() === index))?.proposals;
if (!bountyProposals) {
throw new Error('Unable to find proposal');
}
return bountyProposals[0];
}
export async function multiProposeMotion (api: ApiPromise, submittableExtrinsicArray: SubmittableExtrinsic<'promise'>[], signer: KeyringPair): Promise<void> {
const proposeExtrinsicArray =
submittableExtrinsicArray.map((extrinsic) =>
api.tx.council.propose(4, extrinsic, LENGTH_BOUND));
await execute(api.tx.utility.batch(proposeExtrinsicArray), signer);
}
export async function multiGetMotion (api: ApiPromise, indexes: number[]): Promise<DeriveCollectiveProposal[]> {
const bounties = await api.derive.bounties.bounties();
const bountyProposals =
indexes.map((index) =>
bounties.find((bounty) =>
(bounty.index.toNumber() === index)
)?.proposals
);
return bountyProposals
.map((arr) => arr?.[0])
.filter((arr): arr is DeriveCollectiveProposal => !!arr);
}
async function multiVoteAye (acceptMotionSigners: KeyringPair[], api: ApiPromise, indexes: number[], hashes: Hash[]) {
await Promise.all(
acceptMotionSigners.map((signer) =>
execute(
api.tx.utility.batch(
indexes.map((bountyIndex, i) => api.tx.council.vote(hashes[i], bountyIndex, true))
),
signer
)
)
);
}
async function multiCloseMotion (api: ApiPromise, indexes: number[], hashes: Hash[]) {
await execute(
api.tx.utility.batch(
indexes.map((bountyIndex, i) => api.tx.council.close(hashes[i], bountyIndex, { refTime: WEIGHT_BOUND }, LENGTH_BOUND))),
charlieSigner()
);
}
export async function multiAcceptMotion (api: ApiPromise, hashes: Hash[], indexes: number[]): Promise<void> {
const acceptMotionSigners = [charlieSigner(), daveSigner(), eveSigner(), ferdieSigner()];
await multiVoteAye(acceptMotionSigners, api, indexes, hashes);
await multiCloseMotion(api, indexes, hashes);
}
export function extractIndexesFromProposals (bountyProposals: DeriveCollectiveProposal[]): number[] {
return bountyProposals.map((proposal) => proposal.votes?.index.toNumber() ?? 0);
}
export function extractHashesFromProposals (bountyProposals: DeriveCollectiveProposal[]): Hash[] {
return bountyProposals.map((proposal) => proposal.hash);
}
@@ -0,0 +1,85 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import { execute } from '@pezkuwi/test-support/transaction';
import { BN } from '@pezkuwi/util';
import { waitForBountyState, waitForClaim } from './bountyWaitFunctions.js';
import { FUNDING_TIME, PAYOUT_TIME } from './constants.js';
import { extractHashesFromProposals, extractIndexesFromProposals, fillTreasury, multiAcceptMotion, multiGetMotion, multiProposeMotion } from './helpers.js';
export async function multiProposeBounty (api: ApiPromise, numberOfBounties: number, signer: KeyringPair): Promise<number[]> {
const initialIndex = await api.query.bounties.bountyCount();
const arr = Array.from({ length: numberOfBounties }, (_, i) => api.tx.bounties.proposeBounty(new BN(500_000_000_000_000), `new bounty no ${i}`));
await execute(
api.tx.utility.batch(arr),
signer
);
const endIndex = await api.query.bounties.bountyCount();
if ((endIndex.sub(initialIndex)).toNumber() !== numberOfBounties) {
throw new Error('Multi Propose Failed');
}
return Array.from({ length: numberOfBounties }, (_, i) => i + initialIndex.toNumber());
}
export async function multiApproveBounty (api: ApiPromise, bountyIndexes: number[], signer: KeyringPair): Promise<void> {
const extrinsicArray = bountyIndexes.map((index) => api.tx.bounties.approveBounty(index));
await multiProposeMotion(api, extrinsicArray, signer);
const bountyProposals = await multiGetMotion(api, bountyIndexes);
await fillTreasury(api, signer);
await multiAcceptMotion(api, extractHashesFromProposals(bountyProposals), extractIndexesFromProposals(bountyProposals));
}
export async function multiWaitForBountyFunded (api: ApiPromise, bountyIndexes: number[]): Promise<void> {
const waitFunctions = bountyIndexes.map((bountyIndex) =>
waitForBountyState(api, 'isFunded', bountyIndex, { interval: 2000, timeout: FUNDING_TIME }));
await Promise.all(waitFunctions);
}
export async function multiProposeCurator (api: ApiPromise, bountyIndexes: number[], signer: KeyringPair): Promise<void> {
const extrinsicArray = bountyIndexes.map((index) => api.tx.bounties.proposeCurator(index, signer.address, 10));
await multiProposeMotion(api, extrinsicArray, signer);
const bountyProposals = await multiGetMotion(api, bountyIndexes);
await multiAcceptMotion(api, extractHashesFromProposals(bountyProposals), extractIndexesFromProposals(bountyProposals));
}
export async function multiAcceptCurator (api: ApiPromise, bountyIndexes: number[], signer: KeyringPair): Promise<void> {
await execute(
api.tx.utility.batch(bountyIndexes.map((bountyIndex) => api.tx.bounties.acceptCurator(bountyIndex))),
signer
);
}
export async function multiAwardBounty (api: ApiPromise, bountyIndexes: number[], signer: KeyringPair): Promise<void> {
await execute(
api.tx.utility.batch(bountyIndexes.map((bountyIndex) => api.tx.bounties.awardBounty(bountyIndex, signer.address))),
signer
);
}
export async function multiWaitForClaim (api: ApiPromise, bountyIndexes: number[]): Promise<void> {
for (const index of bountyIndexes) {
await waitForClaim(api, index, { interval: 2000, timeout: PAYOUT_TIME });
}
}
export async function multiClaimBounty (api: ApiPromise, bountyIndexes: number[], signer: KeyringPair): Promise<void> {
await execute(
api.tx.utility.batch(bountyIndexes.map((bountyIndex) => api.tx.bounties.claimBounty(bountyIndex))),
signer
);
}
+18
View File
@@ -0,0 +1,18 @@
## Scripts for bounty testing
The scripts are prepared to run on a local, development version of bizinikiwi with following changes:
- `bin/node/runtime/src/lib.rs`
```
pub const SpendPeriod: BlockNumber = 1 * MINUTES;
pub const BountyDepositPayoutDelay: BlockNumber = 1 * MINUTES
```
To run a script enter the `packages/test-support` directory and run:
```
ts-node scripts/<script-name>
```
Available scripts:
- `createBounties` - creates a list of bounties,
one in each status ( Proposed, Funded, Curator Proposed, Active, Pending Payout, Closed )
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ApiPromise } from '@pezkuwi/api';
import { WsProvider } from '@pezkuwi/rpc-provider';
import { BIZINIKIWI_PORT } from '../bizinikiwi/index.js';
export async function createApi (port: number = BIZINIKIWI_PORT): Promise<ApiPromise> {
process.env.NODE_ENV = 'test';
const provider = new WsProvider(`ws://127.0.0.1:${port}`);
const api = await ApiPromise.create({ provider });
const [chain, nodeName, nodeVersion] = await Promise.all([
api.rpc.system.chain(),
api.rpc.system.name(),
api.rpc.system.version()
]);
console.log(`You are connected to chain ${chain.toString()} using ${nodeName.toString()} v${nodeVersion.toString()}`);
return api;
}
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registry } from '@pezkuwi/types/types';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { Metadata, TypeRegistry } from '@pezkuwi/types';
import metaStatic from '@pezkuwi/types-support/metadata/static-bizinikiwi';
export function createAugmentedApi (): ApiPromise {
const registry = new TypeRegistry();
// FIXME - ref: https://github.com/pezkuwi-js/apps/pull/11051
// Adding support for CJS and ESM correctly has caused some build issues.
// This is a hacky type cast to allow the compiler to be happy.
const metadata = new Metadata(registry as unknown as Registry, metaStatic);
registry.setMetadata(metadata);
const api = new ApiPromise({ provider: new WsProvider('ws://', false), registry: registry as unknown as Registry });
// eslint-disable-next-line deprecation/deprecation
api.injectMetadata(metadata, true);
return api;
}
+7
View File
@@ -0,0 +1,7 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/api-augment/bizinikiwi';
export * from './createApi.js';
export * from './createAugmentedApi.js';
@@ -0,0 +1,35 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll, DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import type { UseAccountInfo } from '@pezkuwi/react-hooks/types';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import type { AccountOverrides, Override } from '../types.js';
export const anAccount = (): AccountOverrides => ({});
export const anAccountWithBalance = (balance: Override<DeriveBalancesAll>): AccountOverrides => ({
balance
});
export const anAccountWithInfo = (info: Override<UseAccountInfo>): AccountOverrides => ({
info
});
export const anAccountWithMeta = (meta: Override<KeyringJson$Meta>): AccountOverrides => ({
meta
});
export const anAccountWithStaking = (staking: Override<DeriveStakingAccount>): AccountOverrides => ({
staking
});
export const anAccountWithBalanceAndMeta = (balance: Override<DeriveBalancesAll>, meta: Override<KeyringJson$Meta>): AccountOverrides => ({
balance,
meta
});
export const anAccountWithInfoAndMeta = (info: Override<UseAccountInfo>, meta: Override<KeyringJson$Meta>): AccountOverrides => ({
info,
meta
});
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registry } from '@pezkuwi/types/types';
import { TypeRegistry, u128 as U128 } from '@pezkuwi/types';
export function balanceOf (number: number | string): U128 {
// FIXME - ref: https://github.com/pezkuwi-js/apps/pull/11051
// Adding support for CJS and ESM correctly has caused some build issues.
// This is a hacky type cast to allow the compiler to be happy.
return new U128(new TypeRegistry() as unknown as Registry, number);
}
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { BountyIndex } from '@pezkuwi/types/interfaces';
import type { PalletBountiesBounty, PalletBountiesBountyStatus } from '@pezkuwi/types/lookup';
import type { Registry } from '@pezkuwi/types/types';
import { balanceOf } from './balance.js';
export class BountyFactory {
readonly #api: ApiPromise;
readonly #registry: Registry;
constructor (api: ApiPromise) {
this.#api = api;
this.#registry = this.#api.registry;
}
public aBountyIndex = (index = 0): BountyIndex =>
this.#registry.createType('BountyIndex', index);
public defaultBounty = (): PalletBountiesBounty =>
this.#registry.createType<PalletBountiesBounty>('Bounty');
public aBountyStatus = (status: string): PalletBountiesBountyStatus =>
this.#registry.createType('PalletBountiesBountyStatus', status);
public bountyStatusWith = ({ curator = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', status = 'Active', updateDue = 100000 } = {}): PalletBountiesBountyStatus => {
if (status === 'Active') {
return this.#registry.createType('PalletBountiesBountyStatus', { active: { curator, updateDue }, status });
}
if (status === 'CuratorProposed') {
return this.#registry.createType('PalletBountiesBountyStatus', { curatorProposed: { curator }, status });
}
throw new Error('Unsupported status');
};
public bountyWith = ({ status = 'Proposed', value = 1 } = {}): PalletBountiesBounty =>
this.aBounty({ status: this.aBountyStatus(status), value: balanceOf(value) });
public aBounty = ({ fee = balanceOf(10), status = this.aBountyStatus('Proposed'), value = balanceOf(500) }: Partial<PalletBountiesBounty> = {}): PalletBountiesBounty =>
this.#registry.createType<PalletBountiesBounty>('Bounty', { fee, status, value });
}
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll, DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import type { UseAccountInfo } from '@pezkuwi/react-hooks/types';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import type { AccountOverrides as ContactOverrides, Override } from '../types.js';
export const aContact = (): ContactOverrides => ({});
export const aContactWithBalance = (balance: Override<DeriveBalancesAll>): ContactOverrides => ({
balance
});
export const aContactWithInfo = (info: Override<UseAccountInfo>): ContactOverrides => ({
info
});
export const aContactWithStaking = (staking: Override<DeriveStakingAccount>): ContactOverrides => ({
staking
});
export const aContactWithMeta = (meta: Override<KeyringJson$Meta>): ContactOverrides => ({
meta
});
@@ -0,0 +1,15 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Hash } from '@pezkuwi/types/interfaces';
import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config';
import { TypeRegistry } from '@pezkuwi/types/create';
export function aGenesisHash (): Hash {
return new TypeRegistry().createType('Hash', PEZKUWI_GENESIS);
}
export function aHash (): Hash {
return new TypeRegistry().createType('Hash');
}
@@ -0,0 +1,20 @@
// Copyright 2017-2025 @pezkuwi/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PalletStakingStakingLedger } from '@pezkuwi/types/lookup';
import { TypeRegistry } from '@pezkuwi/types/create';
import { BN } from '@pezkuwi/util';
export function makeStakingLedger (active: BN | number | string): PalletStakingStakingLedger {
const reg = new TypeRegistry();
// Constructing the whole StakingLedger structure is hard,
// so we fill out just the fields that are definitely required,
// and hope that nothing more is required.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
return {
active: reg.createType('Compact<Balance>', reg.createType('Balance', new BN(active)))
} as PalletStakingStakingLedger;
}
@@ -0,0 +1,39 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types';
import { BN_ONE, BN_ZERO } from '@pezkuwi/util';
import { alice, bob } from '../keyring/addresses.js';
import { balanceOf } from './balance.js';
import { aHash } from './hashes.js';
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,
index: 0,
nays,
threshold: 4
})
})
};
}
export const defaultTreasury = {
burn: BN_ONE,
spendPeriod: BN_ZERO,
value: balanceOf(1)
};
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { extractTime } from '@pezkuwi/util';
import { defaultTreasury } from '../creation/treasury.js';
import { defaultMembers } from '../keyring/addresses.js';
export const mockHooks = {
blockTime: [50, '', extractTime(1)],
members: defaultMembers,
treasury: defaultTreasury
};
@@ -0,0 +1,34 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringJson, KeyringStore } from '@pezkuwi/ui-keyring/types';
type AccountsMap = Record<string, KeyringJson>;
export class MemoryStore implements KeyringStore {
private accounts: AccountsMap = {};
all (cb: (key: string, value: KeyringJson) => void): void {
Object.keys(this.accounts).forEach((accountsKey) => cb(accountsKey, this.accounts[accountsKey]));
}
get (key: string, cb: (value: KeyringJson) => void): void {
cb(this.accounts[key]);
}
remove (key: string, cb: (() => void) | undefined): void {
delete this.accounts[key];
if (cb) {
cb();
}
}
set (key: string, value: KeyringJson, cb: (() => void) | undefined): void {
this.accounts[key] = value;
if (cb) {
cb();
}
}
}
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export const alice = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
export const bob = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
export const charlie = '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy';
export const ferdie = '5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL';
export const defaultMembers = { isMember: true, members: [alice, bob, ferdie] };
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './addresses.js';
export * from './MemoryStore.js';
export * from './signers.js';
@@ -0,0 +1,42 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair } from '@pezkuwi/keyring/types';
import { Keyring } from '@pezkuwi/keyring';
export function aliceSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Alice');
}
export function bobSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Bob');
}
export function charlieSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Charlie');
}
export function daveSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Dave');
}
export function eveSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Eve');
}
export function ferdieSigner (): KeyringPair {
const keyring = new Keyring({ type: 'sr25519' });
return keyring.addFromUri('//Ferdie');
}
@@ -0,0 +1,47 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registrar } from '@pezkuwi/react-hooks/types';
import { statics } from '@pezkuwi/react-api';
import { bob, charlie, ferdie } from '../keyring/index.js';
export const mockRegistration = {
judgements: [
[
statics.registry.createType('RegistrarIndex', '0'),
{
isReasonable: true
}
],
[
statics.registry.createType('RegistrarIndex', '1'),
{
isKnownGood: true
}
],
[
statics.registry.createType('RegistrarIndex', '2'),
{
isErroneous: true
}
],
[
statics.registry.createType('RegistrarIndex', '3'),
{
isReasonable: true
}
]
]
};
export const bobRegistrar: Registrar = { address: bob, index: 0 };
export const charlieRegistrar: Registrar = { address: charlie, index: 1 };
export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 };
export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar];
export const bobShortAddress = '5FHneW…M694ty';
export const charlieShortAddress = '5DAAnr…3PTXFy';
export const ferdieShortAddress = '5CiPPs…SK2DjL';
+260
View File
@@ -0,0 +1,260 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global jest, fail */
import type { RenderResult } from '@testing-library/react';
import type { ApiProps } from '@pezkuwi/react-api/types';
import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@pezkuwi/react-components/Status/types';
import type { UseAccountInfo } from '@pezkuwi/react-hooks/types';
import type { AccountOverrides } from '../utils/accountDefaults.js';
import { queryByAttribute, render, screen } 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 { AccountSidebar, lightTheme } from '@pezkuwi/react-components';
import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue';
import { TypeRegistry } from '@pezkuwi/types/create';
import { keyring } from '@pezkuwi/ui-keyring';
import { BN } from '@pezkuwi/util';
import { alice, bob, charlie, ferdie } from '../keyring/index.js';
import { Table } from '../pagesElements/index.js';
import { mockAccountHooks } from '../utils/accountDefaults.js';
import { mockApiHooks } from '../utils/mockApiHooks.js';
let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
class NotYetRendered extends Error {
}
jest.mock('@pezkuwi/react-hooks/useAccounts', () => ({
useAccounts: () => mockAccountHooks.useAccounts
}));
jest.mock('@pezkuwi/react-hooks/useAccountInfo', () => {
// eslint-disable-next-line func-call-spacing
const actual = jest.requireActual<{useAccountInfo: (address: string) => UseAccountInfo}>('@pezkuwi/react-hooks/useAccountInfo');
return ({
useAccountInfo: (address: string) => {
const mockInfo = mockAccountHooks.accountsMap[address];
return mockInfo
? {
...actual.useAccountInfo(address),
flags: { ...actual.useAccountInfo(address).flags, ...(mockInfo.info.flags) },
identity: {
...actual.useAccountInfo(address).identity,
...(mockInfo.info.identity),
judgements: [
...(actual.useAccountInfo(address).identity?.judgements || []),
...(mockApiHooks.judgements || [])
]
},
tags: [...actual.useAccountInfo(address).tags, ...(mockInfo.info.tags)]
}
: actual.useAccountInfo(address);
}
});
});
jest.mock('@pezkuwi/react-hooks/useNextTick', () => ({
useNextTick: () => true
}));
jest.mock('@pezkuwi/react-hooks/useBalancesAll', () => ({
useBalancesAll: (address: string) => mockAccountHooks.accountsMap[address].balance
}));
jest.mock('@pezkuwi/react-hooks/useStakingInfo', () => ({
useStakingInfo: (address: string) => mockAccountHooks.accountsMap[address].staking
}));
jest.mock('@pezkuwi/react-hooks/useBestNumber', () => ({
useBestNumber: () => 1
}));
jest.mock('@pezkuwi/react-hooks/useSubidentities', () => ({
useSubidentities: () => mockApiHooks.subs
}));
jest.mock('@pezkuwi/app-accounts/Accounts/useMultisigApprovals', () => ({
__esModule: true,
default: () => mockApiHooks.multisigApprovals
}));
jest.mock('@pezkuwi/react-hooks/useDelegations', () => ({
useDelegations: () => mockApiHooks.delegations
}));
jest.mock('@pezkuwi/react-hooks/useProxies', () => ({
useProxies: () => mockApiHooks.proxies
}));
jest.mock('@pezkuwi/react-hooks/useSubidentities', () => ({
useSubidentities: () => mockApiHooks.subs
}));
jest.mock('@pezkuwi/react-hooks/useRegistrars', () => ({
useRegistrars: () => ({
isRegistrar: false,
registrars: mockApiHooks.registrars
})
}));
jest.mock('@pezkuwi/react-hooks/useTheme', () => ({
useTheme: () => ({
theme: 'light',
themeClassName: 'theme--light'
})
}));
export abstract class Page {
private renderResult?: RenderResult;
protected readonly defaultAddresses = [alice, bob, charlie, ferdie];
protected constructor (private readonly overview: React.ReactElement, private readonly rowClassName: string) {
this.overview = overview;
this.rowClassName = rowClassName;
}
render (accounts: [string, AccountOverrides][]): void {
mockAccountHooks.setAccounts(accounts);
accounts.forEach(([address, { meta }]) => {
keyring.addExternal(address, meta);
});
const noop = () => Promise.resolve(() => { /**/ });
const registry = new TypeRegistry();
const api = {
consts: {
babe: {
expectedBlockTime: new BN(1)
},
democracy: {
enactmentPeriod: new BN(1)
},
proxy: {
proxyDepositBase: new BN(1),
proxyDepositFactor: new BN(1)
}
},
createType: () => ({
defKeys: []
}),
derive: {
accounts: {
info: noop
},
balances: {
all: noop
},
chain: {
bestNumber: noop
},
democracy: {
locks: noop
},
staking: {
account: noop
}
},
genesisHash: registry.createType('Hash', PEZKUWI_GENESIS),
query: {
democracy: {
votingOf: noop
},
identity: {
identityOf: noop
}
},
registry: {
chainDecimals: [12],
chainTokens: ['Unit'],
createType: (...args: Parameters<typeof registry.createType>) =>
registry.createType(...args),
lookup: {
names: []
}
},
tx: {
council: {},
democracy: {
delegate: noop
},
multisig: {
approveAsMulti: Object.assign(noop, { meta: { args: [] } })
},
proxy: {
removeProxies: noop
},
utility: noop
}
};
const mockApi: ApiProps = {
api,
apiSystem: {
...api,
isReady: Promise.resolve(api)
},
isApiConnected: true,
isApiInitialized: true,
isApiReady: true,
isEthereum: false,
systemName: 'bizinikiwi'
} as unknown as ApiProps;
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
const queue = {
queueExtrinsic
} as QueueProps;
this.renderResult = render(
<>
<div id='tooltips' />
<Suspense fallback='...'>
<QueueCtx.Provider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiCtx.Provider value={mockApi}>
<AccountSidebar>
{React.cloneElement(this.overview, { onStatusChange: noop }) }
</AccountSidebar>
</ApiCtx.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueCtx.Provider>
</Suspense>
</>
);
}
async getTable (): Promise<Table> {
this.assertRendered();
return new Table(await screen.findByRole('table'), this.rowClassName);
}
clearAccounts (): void {
this.defaultAddresses.forEach((address) => keyring.forgetAccount(address));
}
getById (id: string | RegExp): HTMLElement | null {
this.assertRendered();
const getById = queryByAttribute.bind(null, 'id');
return getById(this.renderResult?.container ?? fail('Page render failed'), id);
}
protected assertRendered (): void {
if (this.renderResult === undefined) {
throw new NotYetRendered();
}
}
}
@@ -0,0 +1,38 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global fail */
import { fireEvent, screen, within } from '@testing-library/react';
export class JudgementTag {
public judgementTag: HTMLElement;
constructor (judgementTag: HTMLElement) {
this.judgementTag = judgementTag;
}
async assertRegistrars (expectedRegistrars: string[]): Promise<void> {
const popup = await this.openPopup();
for (let index = 0, count = expectedRegistrars.length; index < count; index++) {
await within(popup).findByText(expectedRegistrars[index]);
}
}
async clickRegistrar (registrarName: string): Promise<void> {
const popup = await this.openPopup();
const registrars = await within(popup).findAllByTestId('account-name');
const registrar = registrars.find((reg) => reg.textContent === registrarName) ?? fail('Registrar not found');
fireEvent.click(registrar);
}
private async openPopup (): Promise<HTMLElement> {
fireEvent.click(this.judgementTag);
return screen.findByTestId('popup-window');
}
}
@@ -0,0 +1,100 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global expect */
import type { Balance } from '@pezkuwi/types/interfaces';
import { fireEvent, screen, within } from '@testing-library/react';
import { format } from '../utils/balance.js';
import { Sidebar } from './Sidebar.js';
// utility wrapper over an account item in accounts table, serves basic assertions about an account row
export class Row {
public primaryRow: HTMLElement;
public detailsRow: HTMLElement;
constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) {
this.primaryRow = primaryRow;
this.detailsRow = detailsRow;
}
async assertBalancesTotal (expectedTotalBalance: Balance): Promise<void> {
const actualBalanceText = await this.getBalanceSummary();
const expectedBalanceText = format(expectedTotalBalance);
expect(actualBalanceText).toHaveTextContent(expectedBalanceText);
}
async getBalanceSummary (): Promise<HTMLElement> {
return within(this.primaryRow).findByTestId('balance-summary');
}
async assertAccountName (expectedName: string): Promise<void> {
const accountName = await this.getAccountName();
expect(accountName).toHaveTextContent(expectedName);
}
async assertBalancesDetails (expectedBalanceComponents: { name: string, amount: Balance }[]): Promise<void> {
for (const { amount, name } of expectedBalanceComponents) {
await this.assertBalanceComponent({ amount, name });
}
}
async assertBadge (expectedBadgeName: string): Promise<void> {
await within(this.primaryRow).findByTestId(expectedBadgeName);
}
assertNoBadge (badgeName: string): void {
expect(within(this.primaryRow).queryByTestId(badgeName)).toBeFalsy();
}
async assertTags (expectedTagsContent: string): Promise<void> {
const actualTags = await within(this.detailsRow).findByTestId('tags');
expect(actualTags).toHaveTextContent(expectedTagsContent);
}
async assertShortAddress (expectedShortAddress: string): Promise<void> {
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
expect(actualShortAddress).toHaveTextContent(expectedShortAddress);
}
async expand (): Promise<void> {
const toggle = await within(this.primaryRow).findByTestId('row-toggle');
fireEvent.click(toggle);
}
async getBadge (expectedBadgeName: string): Promise<HTMLElement> {
return within(this.primaryRow).findByTestId(`${expectedBadgeName}-badge`);
}
async openSidebar (): Promise<Sidebar> {
const accountName = await this.getAccountName();
fireEvent.click(accountName);
return new Sidebar(await screen.findByTestId('account-sidebar'));
}
private async assertBalanceComponent (expectedBalanceComponent: { name: string; amount: Balance }): Promise<void> {
const balanceElement = await this.getBalanceElementByLabelName(expectedBalanceComponent.name);
const balanceText = format(expectedBalanceComponent.amount);
expect(balanceElement).toHaveTextContent(balanceText);
}
private async getBalanceElementByLabelName (labelName: string): Promise<ChildNode | null> {
const labelElement = await within(this.detailsRow).findByText(labelName);
return labelElement.nextSibling;
}
private getAccountName (): Promise<HTMLElement> {
return within(this.primaryRow).findByTestId('account-name');
}
}
@@ -0,0 +1,160 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global expect */
import { fireEvent, screen, within } from '@testing-library/react';
import { JudgementTag } from './JudgementTag.js';
export class Sidebar {
public sidebar: HTMLElement;
constructor (sidebar: HTMLElement) {
this.sidebar = sidebar;
}
async changeAccountName (accountName: string): Promise<void> {
this.edit();
await this.typeAccountName(accountName);
this.save();
}
async typeAccountName (accountName: string): Promise<void> {
const accountNameInput = await this.findByTestId('name-input');
fireEvent.change(accountNameInput, { target: { value: accountName } });
}
async selectTag (tagName: string): Promise<void> {
const tagsCombobox = this.openTagsDropdown();
const tagOptions = await within(tagsCombobox).findAllByRole('option');
const tag = tagOptions.find((tag) => tag.textContent === tagName);
if (!tag) {
throw new Error(`Unable to find tag ${tagName}`);
}
fireEvent.click(tag);
}
async assertAccountInput (expectedInput: string): Promise<void> {
const nameInput = await this.findByTestId('name-input');
expect(nameInput).toHaveProperty('value', expectedInput);
}
async assertAccountName (expectedAccountName: string): Promise<void> {
const sideBarAddressSection = await this.findByTestId('sidebar-address-menu');
const sideBarName = await within(sideBarAddressSection).findByTestId('account-name');
expect(sideBarName).toHaveTextContent(expectedAccountName);
}
async assertJudgement (judgement: string): Promise<void> {
const judgementsSection = await this.findByTestId('judgements');
expect(judgementsSection).toHaveTextContent(judgement);
}
async assertTags (tagsContent: string): Promise<void> {
const sideBarTags = await this.findByTestId('sidebar-tags');
expect(sideBarTags).toHaveTextContent(tagsContent);
}
close (): Promise<void> {
return this.clickByTestId('close-sidebar-button');
}
cancel (): void {
this.clickButton('Cancel');
}
edit (): void {
this.clickButton('Edit');
}
save (): void {
this.clickButton('Save');
}
async clickByText (text: string): Promise<void> {
const htmlElement = await this.findByText(text);
fireEvent.click(htmlElement);
}
async clickByTestId (testId: string): Promise<void> {
const htmlElement = await this.findByTestId(testId);
fireEvent.click(htmlElement);
}
async findByText (text: string): Promise<HTMLElement> {
return within(this.sidebar).findByText(text);
}
async findByTestId (testId: string): Promise<HTMLElement> {
return within(this.sidebar).findByTestId(testId);
}
async findByRole (role: string): Promise<HTMLElement> {
return within(this.sidebar).findByRole(role);
}
async findAllByRole (role: string): Promise<HTMLElement[]> {
return within(this.sidebar).findAllByRole(role);
}
getByTestId (testId: string): HTMLElement {
return within(this.sidebar).getByTestId(testId);
}
getByRole (roleName: string, options?: Record<string, unknown>): HTMLElement {
return within(this.sidebar).getByRole(roleName, options);
}
queryByRole (roleName: string, options?: Record<string, unknown>): HTMLElement | null {
return within(this.sidebar).queryByRole(roleName, options);
}
queryByTestId (testId: string): HTMLElement | null {
return within(this.sidebar).queryByTestId(testId);
}
async findSubs (): Promise<HTMLElement[]> {
const identitySection = await this.findByTestId('identity-section');
return within(identitySection).queryAllByTestId('subs');
}
async openSubsModal (): Promise<HTMLElement> {
const identitySection = await this.findByTestId('identity-section');
const showSubsButton = await within(identitySection).findByText('Show list');
fireEvent.click(showSubsButton);
return screen.findByTestId('modal');
}
async getJudgement (judgementName: string): Promise<JudgementTag> {
const judgements = await this.findByTestId('judgements');
return new JudgementTag(await within(judgements).findByText(judgementName));
}
private clickButton (buttonName: string) {
const button = this.getByRole('button', { name: buttonName });
fireEvent.click(button);
}
private openTagsDropdown (): HTMLElement {
const tagsDropdown = this.getByRole('combobox', { expanded: false });
fireEvent.click(tagsDropdown);
return tagsDropdown;
}
}
@@ -0,0 +1,65 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global expect */
import { within } from '@testing-library/react';
import { showBalance } from '../utils/balance.js';
import { Row } from './Row.js';
export class Table {
constructor (private readonly table: HTMLElement, private readonly rowClassName: string) {
this.table = table;
this.rowClassName = rowClassName;
}
async assertRowsOrder (balancesExpectedOrder: number[]): Promise<void> {
const orderedRows = await this.getRows();
for (let index = 0; index < orderedRows.length; index++) {
const row = orderedRows[index];
const expectedBalanceTextContent = showBalance(balancesExpectedOrder[index]);
expect(await row.getBalanceSummary()).toHaveTextContent(expectedBalanceTextContent);
}
}
async getRows (): Promise<Row[]> {
const htmlRows = await this.getFilteredHtmlRows();
const collapsibleRows: Row[] = [];
for (let rowIdx = 0; rowIdx < htmlRows.length; rowIdx = rowIdx + 2) {
const primaryRow = htmlRows[rowIdx];
const detailsRow = htmlRows[rowIdx + 1];
collapsibleRows.push(new Row(primaryRow, detailsRow));
}
return collapsibleRows;
}
assertColumnNotExist (columnName: string): void {
expect(within(this.table).queryByRole('columnheader', { name: columnName })).toBeFalsy();
}
assertColumnExists (columnName: string): void {
expect(within(this.table).getByRole('columnheader', { name: columnName })).toBeTruthy();
}
async assertText (text: string): Promise<HTMLElement> {
return within(this.table).findByText(text);
}
private async getFilteredHtmlRows (): Promise<HTMLElement[]> {
const htmlRows = await this.getAllHtmlRows();
return htmlRows.filter((row) => row.className.startsWith(this.rowClassName));
}
private async getAllHtmlRows (): Promise<HTMLElement[]> {
const tableBody = this.table.getElementsByTagName('tbody')[0];
return within(tableBody).findAllByRole('row');
}
}
@@ -0,0 +1,7 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './JudgementTag.js';
export * from './Row.js';
export * from './Sidebar.js';
export * from './Table.js';
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PropsWithChildren } from 'react';
import type React from 'react';
import { useApi } from '@pezkuwi/react-hooks';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
export const WaitForApi = ({ children }: { children: React.ReactNode }): PropsWithChildren<any> | null => {
const api = useApi();
return api.isApiReady ? (children) : null;
};
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './apiInTests.js';
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export const BIZINIKIWI_PORT = Number.parseInt(process.env.TEST_BIZINIKIWI_PORT || '30333');
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './constants.js';
@@ -0,0 +1,40 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { KeyringPair } from '@pezkuwi/keyring/types';
import type { EventRecord, ExtrinsicStatus } from '@pezkuwi/types/interfaces';
import { waitFor } from '../utils/waitFor.js';
export async function execute (extrinsic: SubmittableExtrinsic<'promise'>, signer: KeyringPair, logger = { info: console.log }): Promise<void> {
let currentTxDone = false;
function sendStatusCb ({ events = [], status }: { events?: EventRecord[], status: ExtrinsicStatus; }) {
if (status.isInvalid) {
logger.info('Transaction invalid');
currentTxDone = true;
} else if (status.isReady) {
logger.info('Transaction is ready');
} else if (status.isBroadcast) {
logger.info('Transaction has been broadcasted');
} else if (status.isInBlock) {
logger.info('Transaction is in block');
} else if (status.isFinalized) {
logger.info(`Transaction has been included in blockHash ${status.asFinalized.toHex()}`);
events.forEach(
({ event }) => {
if (event.method === 'ExtrinsicSuccess') {
logger.info('Transaction succeeded');
} else if (event.method === 'ExtrinsicFailed') {
logger.info('Transaction failed');
}
}
);
currentTxDone = true;
}
}
await extrinsic.signAndSend(signer, sendStatusCb);
await waitFor(() => currentTxDone, { timeout: 20000 });
}
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './execute.js';
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll, DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import type { UseAccountInfo } from '@pezkuwi/react-hooks/types';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
export type Override<T> = {
[P in keyof T]?: T[P];
}
export interface AccountOverrides {
meta?: Override<KeyringJson$Meta>;
balance?: Override<DeriveBalancesAll>;
staking?: Override<DeriveStakingAccount>;
info?: Override<UseAccountInfo>;
}
export interface WaitOptions { interval?: number, timeout?: number }
@@ -0,0 +1,139 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll, DeriveStakingAccount } from '@pezkuwi/api-derive/types';
import type { Accounts } from '@pezkuwi/react-hooks/ctx/types';
import type { UseAccountInfo } from '@pezkuwi/react-hooks/types';
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import { BN } from '@pezkuwi/util';
import { balanceOf } from '../creation/balance.js';
import { makeStakingLedger } from '../creation/staking.js';
export interface Account {
balance: DeriveBalancesAll,
info: UseAccountInfo,
staking: DeriveStakingAccount
}
export type AccountsMap = Record<string, Account>;
export type Override<T> = {
[P in keyof T]?: T[P];
}
/**
* Test inputs structure
*/
export interface AccountOverrides {
meta?: Override<KeyringJson$Meta>;
balance?: Override<DeriveBalancesAll>;
staking?: Override<DeriveStakingAccount>;
info?: Override<UseAccountInfo>;
}
export const emptyAccounts: Accounts = {
allAccounts: [],
allAccountsHex: [],
areAccountsLoaded: true,
hasAccounts: false,
isAccount: () => true
};
// here it's extremely hard to reconstruct the entire DeriveBalancesAll upfront, so we incrementally add properties
// instead along the way; thus the need to tell the tsc we know what we are doing here
export const defaultBalanceAccount = {
accountNonce: new BN(1),
additional: [],
availableBalance: balanceOf(0),
freeBalance: balanceOf(0),
lockedBalance: balanceOf(0),
lockedBreakdown: [],
namedReserves: [],
reservedBalance: balanceOf(0)
} as unknown as DeriveBalancesAll;
// here it's extremely hard to reconstruct the entire DeriveStakingAccount upfront,
// so we set just the properties that we use in page-accounts
export const defaultStakingAccount = {
nextSessionIds: [],
nominators: [],
redeemable: balanceOf(0),
sessionIds: [],
stakingLedger: makeStakingLedger(0),
unlocking: [
{
remainingEras: new BN('1000000000'),
value: balanceOf(0)
},
{
remainingEras: new BN('2000000000'),
value: balanceOf(0)
},
{
remainingEras: new BN('3000000000'),
value: balanceOf(0)
}
]
} as unknown as DeriveStakingAccount;
export const defaultMeta: KeyringJson$Meta = {};
export const defaultAccountInfo = {
flags: {},
identity: { email: 'user@email.com', isExistent: true, judgements: [] },
tags: []
} as unknown as UseAccountInfo;
class MockAccountHooks {
public useAccounts: Accounts = emptyAccounts;
public accountsMap: AccountsMap = {};
public nonce: BN = new BN(1);
public setAccounts (accounts: [string, AccountOverrides][]): void {
this.useAccounts = {
allAccounts: accounts.map(([address]) => address),
allAccountsHex: [],
areAccountsLoaded: true,
hasAccounts: accounts && accounts.length !== 0,
isAccount: () => true
};
for (const [address, props] of accounts) {
const staking = { ...defaultStakingAccount };
const meta = { ...defaultMeta };
const balance = { ...defaultBalanceAccount };
const info = { ...defaultAccountInfo };
Object
.entries(props.meta || meta)
.forEach(([key, value]) => {
(meta as Record<string, unknown>)[key] = value;
});
Object
.entries(props.balance || balance)
.forEach(([key, value]) => {
(balance as Record<string, unknown>)[key] = value;
});
Object
.entries(props.staking || staking)
.forEach(([key, value]) => {
(staking as Record<string, unknown>)[key] = value;
});
Object
.entries(props.info || info)
.forEach(([key, value]) => {
(info as Record<string, unknown>)[key] = value;
});
this.accountsMap[address] = {
balance,
info,
staking
};
}
}
}
export const mockAccountHooks = new MockAccountHooks();
@@ -0,0 +1,29 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Balance } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
import { formatBalance } from '@pezkuwi/util';
import { balanceOf } from '../creation/balance.js';
/**
* Creates a balance instance for testing purposes which most often do not need to specify/use decimal part.
* @param amountInt Integer part of the balance number
* @param decimalsString Decimals part of the balance number. Note! This is a string sequence just after '.' separator
* that is the point that separates integers from decimals. E.g. (100, 4567) => 100.45670000...00
*/
export function balance (amountInt: number, decimalsString?: string): Balance {
const decimalsPadded = (decimalsString || '').padEnd(12, '0');
return balanceOf(amountInt.toString() + decimalsPadded);
}
export function showBalance (amount: number): string {
return format(balance(amount));
}
export function format (amount: Balance | BN): string {
return formatBalance(amount, { decimals: 12, forceUnit: '-', withUnit: true });
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './accountDefaults.js';
export * from './balance.js';
export * from './mockApiHooks.js';
export * from './renderedScreenUtils.js';
export * from './waitFor.js';
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Registrar } from '@pezkuwi/react-hooks/types';
import type { H256, Multisig, ProxyDefinition, RegistrationJudgement, Voting } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
class MockApiHooks {
public multisigApprovals: [H256, Multisig][] | undefined = [];
public delegations: Voting[] | undefined;
public proxies: [ProxyDefinition[], BN][] | undefined = [];
public subs: string[] | undefined = [];
public judgements: RegistrationJudgement[] | undefined = [];
public registrars: Registrar[] = [];
public setDelegations (delegations: Voting[]) {
this.delegations = delegations;
}
public setMultisigApprovals (multisigApprovals: [H256, Multisig][]) {
this.multisigApprovals = multisigApprovals;
}
public setProxies (proxies: [ProxyDefinition[], BN][]) {
this.proxies = proxies;
}
public setSubs (subs: string[] | undefined) {
this.subs = subs;
}
public setJudgements (judgements: RegistrationJudgement[] | undefined) {
this.judgements = judgements;
}
public setRegistrars (registrars: Registrar[]) {
this.registrars = registrars;
}
}
export const mockApiHooks = new MockApiHooks();
@@ -0,0 +1,28 @@
// Copyright 2017-2025 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global expect */
import { fireEvent, screen } from '@testing-library/react';
export const clickButton = async (buttonName: string): Promise<void> => {
const button = await screen.findByRole('button', { name: buttonName });
fireEvent.click(button);
};
export const assertText = async (text: string): Promise<HTMLElement> => {
return screen.findByText(text);
};
export const fillInput = (inputTestId: string, value: string): void => {
const nameInput = screen.getByTestId(inputTestId);
fireEvent.change(nameInput, { target: { value } });
};
export const assertButtonDisabled = (buttonName: string): void => {
const button = screen.getByRole('button', { name: buttonName });
expect(button).toHaveClass('isDisabled');
};
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { WaitOptions } from '../types.js';
export async function waitFor (predicate: () => Promise<boolean> | boolean, { interval = 500, timeout = 10000 }: WaitOptions = {}): Promise<boolean> {
const asyncPredicate = () => Promise.resolve(predicate());
let elapsed = 0;
while (!(await asyncPredicate())) {
if (elapsed > timeout) {
throw Error('Timeout');
}
await sleep(interval);
elapsed += interval;
}
return true;
}
export function sleep (ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src",
"module": "CommonJS",
"moduleResolution": "node",
"target": "es2018",
/* This is a cjs target, so ignore */
"verbatimModuleSyntax": false
},
"exclude": [
"scripts/*"
],
"references": [
{ "path": "../react-api/tsconfig.build.json" },
{ "path": "../react-api/tsconfig.xref.json" }
],
"ts-node": {
"require": [
"tsconfig-paths/register"
]
}
}