mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-22 05:27:56 +00:00
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:
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user