Files
Claude db05f21e52 feat(tests): add comprehensive test infrastructure based on blockchain pallet tests
Created complete testing framework for web and mobile frontends based on 437 test scenarios extracted from 12 blockchain pallet test files.

Test Infrastructure:
- Mock data generators for all 12 pallets (Identity, Perwerde, Rewards, Treasury, etc.)
- Test helper utilities (async, blockchain mocks, validation, custom matchers)
- Example unit tests for web (KYC Application) and mobile (Education Course List)
- Example E2E tests using Cypress (web) and Detox (mobile)
- Executable test runner scripts with colored output
- Comprehensive documentation with all 437 test scenarios

Coverage:
- pallet-identity-kyc: 39 test scenarios
- pallet-perwerde: 30 test scenarios
- pallet-pez-rewards: 44 test scenarios
- pallet-pez-treasury: 58 test scenarios
- pallet-presale: 24 test scenarios
- pallet-referral: 17 test scenarios
- pallet-staking-score: 23 test scenarios
- pallet-tiki: 66 test scenarios
- pallet-token-wrapper: 18 test scenarios
- pallet-trust: 26 test scenarios
- pallet-validator-pool: 27 test scenarios
- pallet-welati: 65 test scenarios

Files created:
- tests/utils/mockDataGenerators.ts (550+ lines)
- tests/utils/testHelpers.ts (400+ lines)
- tests/web/unit/citizenship/KYCApplication.test.tsx
- tests/mobile/unit/education/CourseList.test.tsx
- tests/web/e2e/cypress/citizenship-kyc.cy.ts
- tests/mobile/e2e/detox/education-flow.e2e.ts
- tests/run-web-tests.sh (executable)
- tests/run-mobile-tests.sh (executable)
- tests/README.md (800+ lines of documentation)
2025-11-21 04:46:35 +00:00

415 lines
10 KiB
TypeScript

/**
* Test Helper Utilities
* Common testing utilities for web and mobile
*/
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { ReactElement, ReactNode } from 'react';
// ============================================================================
// REACT TESTING LIBRARY HELPERS
// ============================================================================
/**
* Custom render function that includes common providers
*/
export interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
initialState?: any;
polkadotConnected?: boolean;
walletConnected?: boolean;
}
export function renderWithProviders(
ui: ReactElement,
options?: CustomRenderOptions
): RenderResult {
const {
initialState = {},
polkadotConnected = true,
walletConnected = true,
...renderOptions
} = options || {};
// Wrapper will be platform-specific (web or mobile)
// This is a base implementation
return render(ui, renderOptions);
}
// ============================================================================
// ASYNC UTILITIES
// ============================================================================
/**
* Wait for a condition to be true
*/
export async function waitForCondition(
condition: () => boolean,
timeout: number = 5000,
interval: number = 100
): Promise<void> {
const startTime = Date.now();
while (!condition()) {
if (Date.now() - startTime > timeout) {
throw new Error('Timeout waiting for condition');
}
await sleep(interval);
}
}
/**
* Sleep for specified milliseconds
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ============================================================================
// BLOCKCHAIN MOCK HELPERS
// ============================================================================
/**
* Mock Polkadot.js API transaction response
*/
export const mockTransactionResponse = (success: boolean = true) => ({
status: {
isInBlock: success,
isFinalized: success,
type: success ? 'InBlock' : 'Invalid',
},
events: success ? [
{
event: {
section: 'system',
method: 'ExtrinsicSuccess',
data: [],
},
},
] : [],
dispatchError: success ? null : {
isModule: true,
asModule: {
index: { toNumber: () => 0 },
error: { toNumber: () => 0 },
},
},
});
/**
* Mock blockchain query response
*/
export const mockQueryResponse = (data: any) => ({
toJSON: () => data,
toString: () => JSON.stringify(data),
unwrap: () => ({ balance: data }),
isEmpty: !data || data.length === 0,
toNumber: () => (typeof data === 'number' ? data : 0),
});
/**
* Generate mock account
*/
export const mockAccount = (address?: string) => ({
address: address || `5${Math.random().toString(36).substring(2, 15)}`,
meta: {
name: 'Test Account',
source: 'polkadot-js',
},
type: 'sr25519',
});
// ============================================================================
// FORM TESTING HELPERS
// ============================================================================
/**
* Fill form field by test ID
*/
export function fillInput(
getByTestId: (testId: string) => HTMLElement,
testId: string,
value: string
): void {
const input = getByTestId(testId) as HTMLInputElement;
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
/**
* Click button by test ID
*/
export function clickButton(
getByTestId: (testId: string) => HTMLElement,
testId: string
): void {
const button = getByTestId(testId);
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
/**
* Check if element has class
*/
export function hasClass(element: HTMLElement, className: string): boolean {
return element.className.includes(className);
}
// ============================================================================
// VALIDATION HELPERS
// ============================================================================
/**
* Validate Polkadot address format
*/
export function isValidPolkadotAddress(address: string): boolean {
return /^5[1-9A-HJ-NP-Za-km-z]{47}$/.test(address);
}
/**
* Validate IPFS CID format
*/
export function isValidIPFSCID(cid: string): boolean {
return /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/.test(cid);
}
/**
* Validate email format
*/
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
/**
* Validate amount format
*/
export function isValidAmount(amount: string): boolean {
return /^\d+(\.\d{1,12})?$/.test(amount);
}
// ============================================================================
// DATA ASSERTION HELPERS
// ============================================================================
/**
* Assert balance matches expected value
*/
export function assertBalanceEquals(
actual: bigint | string,
expected: bigint | string,
decimals: number = 12
): void {
const actualBigInt = typeof actual === 'string' ? BigInt(actual) : actual;
const expectedBigInt = typeof expected === 'string' ? BigInt(expected) : expected;
if (actualBigInt !== expectedBigInt) {
throw new Error(
`Balance mismatch: expected ${expectedBigInt.toString()} but got ${actualBigInt.toString()}`
);
}
}
/**
* Assert percentage within range
*/
export function assertPercentageInRange(
value: number,
min: number,
max: number
): void {
if (value < min || value > max) {
throw new Error(`Percentage ${value} is not within range [${min}, ${max}]`);
}
}
// ============================================================================
// MOCK STATE BUILDERS
// ============================================================================
/**
* Build mock Polkadot context state
*/
export const buildPolkadotContextState = (overrides: Partial<any> = {}) => ({
api: {
query: {},
tx: {},
rpc: {},
isReady: Promise.resolve(true),
},
isApiReady: true,
selectedAccount: mockAccount(),
accounts: [mockAccount()],
connectWallet: jest.fn(),
disconnectWallet: jest.fn(),
error: null,
...overrides,
});
/**
* Build mock wallet context state
*/
export const buildWalletContextState = (overrides: Partial<any> = {}) => ({
isConnected: true,
account: mockAccount().address,
balance: '1000000000000000',
balances: {
HEZ: '1000000000000000',
PEZ: '500000000000000',
wHEZ: '300000000000000',
USDT: '1000000000', // 6 decimals
},
signer: {},
connectWallet: jest.fn(),
disconnect: jest.fn(),
refreshBalances: jest.fn(),
...overrides,
});
// ============================================================================
// ERROR TESTING HELPERS
// ============================================================================
/**
* Expect async function to throw
*/
export async function expectAsyncThrow(
fn: () => Promise<any>,
expectedError?: string | RegExp
): Promise<void> {
try {
await fn();
throw new Error('Expected function to throw, but it did not');
} catch (error: any) {
if (expectedError) {
const message = error.message || error.toString();
if (typeof expectedError === 'string') {
if (!message.includes(expectedError)) {
throw new Error(
`Expected error message to include "${expectedError}", but got "${message}"`
);
}
} else {
if (!expectedError.test(message)) {
throw new Error(
`Expected error message to match ${expectedError}, but got "${message}"`
);
}
}
}
}
}
/**
* Mock console.error to suppress expected errors
*/
export function suppressConsoleError(fn: () => void): void {
const originalError = console.error;
console.error = jest.fn();
fn();
console.error = originalError;
}
// ============================================================================
// TIMING HELPERS
// ============================================================================
/**
* Advance time for Jest fake timers
*/
export function advanceTimersByTime(ms: number): void {
jest.advanceTimersByTime(ms);
}
/**
* Run all pending timers
*/
export function runAllTimers(): void {
jest.runAllTimers();
}
/**
* Clear all timers
*/
export function clearAllTimers(): void {
jest.clearAllTimers();
}
// ============================================================================
// CUSTOM MATCHERS (for Jest)
// ============================================================================
declare global {
namespace jest {
interface Matchers<R> {
toBeValidPolkadotAddress(): R;
toBeValidIPFSCID(): R;
toMatchBalance(expected: bigint | string, decimals?: number): R;
}
}
}
export const customMatchers = {
toBeValidPolkadotAddress(received: string) {
const pass = isValidPolkadotAddress(received);
return {
pass,
message: () =>
pass
? `Expected ${received} not to be a valid Polkadot address`
: `Expected ${received} to be a valid Polkadot address`,
};
},
toBeValidIPFSCID(received: string) {
const pass = isValidIPFSCID(received);
return {
pass,
message: () =>
pass
? `Expected ${received} not to be a valid IPFS CID`
: `Expected ${received} to be a valid IPFS CID`,
};
},
toMatchBalance(received: bigint | string, expected: bigint | string, decimals = 12) {
const receivedBigInt = typeof received === 'string' ? BigInt(received) : received;
const expectedBigInt = typeof expected === 'string' ? BigInt(expected) : expected;
const pass = receivedBigInt === expectedBigInt;
return {
pass,
message: () =>
pass
? `Expected balance ${receivedBigInt} not to match ${expectedBigInt}`
: `Expected balance ${receivedBigInt} to match ${expectedBigInt}`,
};
},
};
// ============================================================================
// TEST DATA CLEANUP
// ============================================================================
/**
* Clean up test data after each test
*/
export function cleanupTestData(): void {
// Clear local storage
if (typeof localStorage !== 'undefined') {
localStorage.clear();
}
// Clear session storage
if (typeof sessionStorage !== 'undefined') {
sessionStorage.clear();
}
// Clear cookies
if (typeof document !== 'undefined') {
document.cookie.split(';').forEach(cookie => {
document.cookie = cookie
.replace(/^ +/, '')
.replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`);
});
}
}