mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-11-03 22:51:06 +00:00
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:
1 parent
8539e30888
commit
0c5b47ce0e
20 files changed
+330
-192
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
};
|
||||
Reference in new issue
Block a user