Integration specs: node:test module mocks instead of jest.mock

The account, address, sidebar and bounties specs replaced hooks with
jest.mock, which the node:test runner does not have, so all five failed
to load and were excluded from yarn test. They now register mocks with
mock.module from node:test and import the page under test afterwards,
since Node only swaps a module that has not been loaded yet.

- test-support/mocks: mockPageHooks() replaces the hooks a Page reads.
  An ES module mock replaces every export, so each mock lists them all
  (useBestNumber's useBestNumberRelay included). The real useAccountInfo
  is wrapped, typed now, with the fixture identity merged in a typed
  helper.
- The page-specific mocks live with their page: useMultisigApprovals in
  page-accounts/test, useAddresses in page-addresses/test.
- jest-dom matchers were never registered for this runner; specs and
  page elements use assertHasClass, assertTextContent and assertVisible
  from test-support, with the same meaning.
- Page's mock api gains apiIdentity, which useAccountInfo reads.
- The bounties voting tooltip renders hidden and fades in, so the spec
  waits for it to become visible.
- jest/expect-expect counts the assert* helpers as assertions; the
  disable comments that only existed for that are removed.

Create and Bounties run green. Accounts, Addresses and Sidebar load and
stay skipped as upstream left them (their FIXMEs are about the reworked
tables). Needs @pezkuwi/dev 0.86.6 for the module mocks flag and the
fuller browser env.
This commit is contained in:
Satoshi Qazi Muhammed authored and SatoshiQaziMuhammed committed 2026-10-10 11:42:13 +04:00
1 parent 8539e30888
commit 0c5b47ce0e
20 files changed
+330 -192

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
// Copyright 2017-2026 @pezkuwi/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AddressIdentity, UseAccountInfo } from '@pezkuwi/react-hooks/types';
import { mock } from 'node:test';
import { mockAccountHooks } from '../utils/accountDefaults.js';
import { mockApiHooks } from '../utils/mockApiHooks.js';
/**
* The fixture identity over the real one, with the fixture judgements added
* to the real ones
*/
function withMockIdentity (actual?: AddressIdentity, mocked?: AddressIdentity): AddressIdentity | undefined {
const identity = mocked
? { ...actual, ...mocked }
: actual;
return identity && {
...identity,
judgements: [
...(actual?.judgements || []),
...(mockApiHooks.judgements || [])
]
};
}
/**
* Replaces the hooks that a Page reads with the shared fixtures in
* mockAccountHooks and mockApiHooks.
*
* Node only swaps a module that has not been loaded yet, so a spec awaits
* this before anything that loads @pezkuwi/react-hooks, and then imports
* the page under test with a dynamic import(). Needs node's
* --experimental-test-module-mocks flag.
*
* A module mock replaces every export of the module, and the barrel in
* @pezkuwi/react-hooks re-exports them by name, so each mock lists all of
* them. Loading the real module to copy the rest is not an option: some of
* them load the barrel, which would bind it before the mocks are in place.
*/
export async function mockPageHooks (): Promise<void> {
mock.module('@pezkuwi/react-hooks/useAccounts', {
namedExports: { useAccounts: () => mockAccountHooks.useAccounts }
});
mock.module('@pezkuwi/react-hooks/useNextTick', {
namedExports: { useNextTick: () => true }
});
mock.module('@pezkuwi/react-hooks/useBalancesAll', {
namedExports: { useBalancesAll: (address: string) => mockAccountHooks.accountsMap[address].balance }
});
mock.module('@pezkuwi/react-hooks/useStakingInfo', {
namedExports: { useStakingInfo: (address: string) => mockAccountHooks.accountsMap[address].staking }
});
mock.module('@pezkuwi/react-hooks/useBestNumber', {
namedExports: {
useBestNumber: () => 1,
useBestNumberRelay: () => 1
}
});
mock.module('@pezkuwi/react-hooks/useSubidentities', {
namedExports: { useSubidentities: () => mockApiHooks.subs }
});
mock.module('@pezkuwi/react-hooks/useDelegations', {
namedExports: { useDelegations: () => mockApiHooks.delegations }
});
mock.module('@pezkuwi/react-hooks/useProxies', {
namedExports: { useProxies: () => mockApiHooks.proxies }
});
mock.module('@pezkuwi/react-hooks/useRegistrars', {
namedExports: {
useRegistrars: () => ({
isRegistrar: false,
registrars: mockApiHooks.registrars
})
}
});
mock.module('@pezkuwi/react-hooks/useTheme', {
namedExports: {
useTheme: () => ({
theme: 'light',
themeClassName: 'theme--light'
})
}
});
// The fixture is layered over the real hook. It does not load the barrel,
// so it can be loaded here, once the hooks above have been replaced.
const { useAccountInfo: useActualAccountInfo } = await import('@pezkuwi/react-hooks/useAccountInfo');
mock.module('@pezkuwi/react-hooks/useAccountInfo', {
namedExports: {
useAccountInfo: (address: string): UseAccountInfo => {
const mockInfo = mockAccountHooks.accountsMap[address];
const info = useActualAccountInfo(address);
return mockInfo
? {
...info,
flags: { ...info.flags, ...(mockInfo.info.flags) },
identity: withMockIdentity(info.identity, mockInfo.info.identity),
tags: [...info.tags, ...(mockInfo.info.tags)]
}
: info;
}
}
});
}
+2 -83
View File
@@ -6,7 +6,6 @@
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';
@@ -25,94 +24,12 @@ 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', () => {
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];
@@ -198,6 +115,8 @@ export abstract class Page {
};
const mockApi: ApiProps = {
api,
// identities live on the people chain; here both are the same mock
apiIdentity: api,
apiSystem: {
...api,
isReady: Promise.resolve(api)
@@ -8,6 +8,7 @@ import type { Balance } from '@pezkuwi/types/interfaces';
import { fireEvent, screen, within } from '@testing-library/react';
import { format } from '../utils/balance.js';
import { assertTextContent } from '../utils/domAssertions.js';
import { Sidebar } from './Sidebar.js';
// utility wrapper over an account item in accounts table, serves basic assertions about an account row
@@ -24,7 +25,7 @@ export class Row {
const actualBalanceText = await this.getBalanceSummary();
const expectedBalanceText = format(expectedTotalBalance);
expect(actualBalanceText).toHaveTextContent(expectedBalanceText);
assertTextContent(actualBalanceText, expectedBalanceText);
}
async getBalanceSummary (): Promise<HTMLElement> {
@@ -34,7 +35,7 @@ export class Row {
async assertAccountName (expectedName: string): Promise<void> {
const accountName = await this.getAccountName();
expect(accountName).toHaveTextContent(expectedName);
assertTextContent(accountName, expectedName);
}
async assertBalancesDetails (expectedBalanceComponents: { name: string, amount: Balance }[]): Promise<void> {
@@ -54,13 +55,13 @@ export class Row {
async assertTags (expectedTagsContent: string): Promise<void> {
const actualTags = await within(this.detailsRow).findByTestId('tags');
expect(actualTags).toHaveTextContent(expectedTagsContent);
assertTextContent(actualTags, expectedTagsContent);
}
async assertShortAddress (expectedShortAddress: string): Promise<void> {
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
expect(actualShortAddress).toHaveTextContent(expectedShortAddress);
assertTextContent(actualShortAddress, expectedShortAddress);
}
async expand (): Promise<void> {
@@ -85,7 +86,7 @@ export class Row {
const balanceElement = await this.getBalanceElementByLabelName(expectedBalanceComponent.name);
const balanceText = format(expectedBalanceComponent.amount);
expect(balanceElement).toHaveTextContent(balanceText);
assertTextContent(balanceElement, balanceText);
}
private async getBalanceElementByLabelName (labelName: string): Promise<ChildNode | null> {
@@ -5,6 +5,7 @@
import { fireEvent, screen, within } from '@testing-library/react';
import { assertTextContent } from '../utils/domAssertions.js';
import { JudgementTag } from './JudgementTag.js';
export class Sidebar {
@@ -48,19 +49,19 @@ export class Sidebar {
const sideBarAddressSection = await this.findByTestId('sidebar-address-menu');
const sideBarName = await within(sideBarAddressSection).findByTestId('account-name');
expect(sideBarName).toHaveTextContent(expectedAccountName);
assertTextContent(sideBarName, expectedAccountName);
}
async assertJudgement (judgement: string): Promise<void> {
const judgementsSection = await this.findByTestId('judgements');
expect(judgementsSection).toHaveTextContent(judgement);
assertTextContent(judgementsSection, judgement);
}
async assertTags (tagsContent: string): Promise<void> {
const sideBarTags = await this.findByTestId('sidebar-tags');
expect(sideBarTags).toHaveTextContent(tagsContent);
assertTextContent(sideBarTags, tagsContent);
}
close (): Promise<void> {
@@ -6,6 +6,7 @@
import { within } from '@testing-library/react';
import { showBalance } from '../utils/balance.js';
import { assertTextContent } from '../utils/domAssertions.js';
import { Row } from './Row.js';
export class Table {
@@ -21,7 +22,7 @@ export class Table {
const row = orderedRows[index];
const expectedBalanceTextContent = showBalance(balancesExpectedOrder[index]);
expect(await row.getBalanceSummary()).toHaveTextContent(expectedBalanceTextContent);
assertTextContent(await row.getBalanceSummary(), expectedBalanceTextContent);
}
}
@@ -0,0 +1,55 @@
// Copyright 2017-2026 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
// DOM checks with the meaning of the matching @testing-library/jest-dom
// matchers, which the node:test based runner does not provide
import assert from 'node:assert/strict';
function describeElement (node: Node): string {
const html = (node as Partial<Element>).outerHTML ?? node.nodeName;
return html.length > 200
? `${html.slice(0, 200)}...`
: html;
}
/** As toHaveClass: the element carries the class */
export function assertHasClass (node: Node | null, className: string): void {
assert.ok(node, `Expected an element with class "${className}", found none`);
const classList = (node as Partial<Element>).classList;
assert.ok(classList, `Expected an element with class "${className}", found ${describeElement(node)}`);
assert.ok(classList.contains(className), `Expected class "${className}" on ${describeElement(node)}`);
}
/** As toHaveTextContent: the whitespace-normalized text contains the string, or matches the pattern */
export function assertTextContent (element: Node | null, expected: string | RegExp): void {
assert.ok(element, `Expected an element with text ${String(expected)}, found none`);
const text = (element.textContent ?? '').replace(/\s+/g, ' ').trim();
if (typeof expected === 'string') {
assert.ok(text.includes(expected), `Expected text containing "${expected}", found "${text}"`);
} else {
assert.match(text, expected);
}
}
/** As toBeVisible: in the document, and neither it nor an ancestor is hidden */
export function assertVisible (element: Element | null): void {
assert.ok(element, 'Expected a visible element, found none');
assert.ok(element.isConnected, `Expected ${describeElement(element)} to be in the document`);
for (let current: Element | null = element; current; current = current.parentElement) {
const style = current.ownerDocument.defaultView?.getComputedStyle(current);
const isHidden = current.hasAttribute('hidden') ||
style?.display === 'none' ||
style?.visibility === 'hidden' ||
style?.visibility === 'collapse' ||
style?.opacity === '0';
assert.ok(!isHidden, `Expected ${describeElement(element)} to be visible, hidden by ${describeElement(current)}`);
}
}
+1
View File
@@ -3,6 +3,7 @@
export * from './accountDefaults.js';
export * from './balance.js';
export * from './domAssertions.js';
export * from './mockApiHooks.js';
export * from './renderedScreenUtils.js';
export * from './waitFor.js';
@@ -1,10 +1,10 @@
// Copyright 2017-2026 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global expect */
import { fireEvent, screen } from '@testing-library/react';
import { assertHasClass } from './domAssertions.js';
export const clickButton = async (buttonName: string): Promise<void> => {
const button = await screen.findByRole('button', { name: buttonName });
@@ -24,5 +24,5 @@ export const fillInput = (inputTestId: string, value: string): void => {
export const assertButtonDisabled = (buttonName: string): void => {
const button = screen.getByRole('button', { name: buttonName });
expect(button).toHaveClass('isDisabled');
assertHasClass(button, 'isDisabled');
};