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

+8
View File
@@ -20,5 +20,13 @@ export default [
'@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/unbound-method': 'off' '@typescript-eslint/unbound-method': 'off'
} }
},
{
// the same files the base config enables the jest plugin for
files: ['**/*.spec.ts', '**/*.spec.tsx'],
rules: {
// the specs assert through expect() and the assert* helpers in test-support
'jest/expect-expect': ['warn', { assertFunctionNames: ['expect', 'assert*', '*.assert*', '*.expect*'] }]
}
} }
]; ];
@@ -17,17 +17,21 @@ import { toShortAddress } from '@pezkuwi/react-components/util';
import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithInfoAndMeta, anAccountWithMeta, anAccountWithStaking } from '@pezkuwi/test-support/creation/account'; import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithInfoAndMeta, anAccountWithMeta, anAccountWithStaking } from '@pezkuwi/test-support/creation/account';
import { makeStakingLedger as ledger } from '@pezkuwi/test-support/creation/staking'; import { makeStakingLedger as ledger } from '@pezkuwi/test-support/creation/staking';
import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring'; import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring';
import { balance, mockApiHooks, showBalance } from '@pezkuwi/test-support/utils'; import { assertHasClass, assertTextContent, balance, mockApiHooks, showBalance } from '@pezkuwi/test-support/utils';
import { TypeRegistry } from '@pezkuwi/types/create'; import { TypeRegistry } from '@pezkuwi/types/create';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
import { BN } from '@pezkuwi/util'; import { BN } from '@pezkuwi/util';
import { AccountsPage } from '../../test/pages/accountsPage.js'; import { mockAccountsPageHooks } from '../../test/mockAccountsPageHooks.js';
await mockAccountsPageHooks();
const { AccountsPage } = await import('../../test/pages/accountsPage.js');
// FIXME isSplit Table // FIXME isSplit Table
// eslint-disable-next-line jest/no-disabled-tests // eslint-disable-next-line jest/no-disabled-tests
describe.skip('Accounts page', () => { describe.skip('Accounts page', () => {
let accountsPage: AccountsPage; let accountsPage: InstanceType<typeof AccountsPage>;
beforeAll(async () => { beforeAll(async () => {
await i18next.changeLanguage('en'); await i18next.changeLanguage('en');
@@ -64,7 +68,6 @@ describe.skip('Accounts page', () => {
expect(accountRows).toHaveLength(0); expect(accountRows).toHaveLength(0);
}); });
// eslint-disable-next-line jest/expect-expect
it('the accounts table contains a message about no accounts available', async () => { it('the accounts table contains a message about no accounts available', async () => {
const noAccountsMessage = 'You don\'t have any accounts. Some features are currently hidden and will only become available once you have accounts.'; const noAccountsMessage = 'You don\'t have any accounts. Some features are currently hidden and will only become available once you have accounts.';
const accountsTable = await accountsPage.getTable(); const accountsTable = await accountsPage.getTable();
@@ -132,7 +135,7 @@ describe.skip('Accounts page', () => {
}); });
// FIXME broken after column rework // FIXME broken after column rework
// eslint-disable-next-line jest/no-disabled-tests, jest/expect-expect // eslint-disable-next-line jest/no-disabled-tests
it.skip('a separate column for parent account is not displayed', async () => { it.skip('a separate column for parent account is not displayed', async () => {
accountsPage.renderDefaultAccounts(1); accountsPage.renderDefaultAccounts(1);
const accountsTable = await accountsPage.getTable(); const accountsTable = await accountsPage.getTable();
@@ -176,11 +179,11 @@ describe.skip('Accounts page', () => {
accountsPage.renderDefaultAccounts(1); accountsPage.renderDefaultAccounts(1);
const row = (await accountsPage.getAccountRows())[0]; const row = (await accountsPage.getAccountRows())[0];
expect(row.detailsRow).toHaveClass('isCollapsed'); assertHasClass(row.detailsRow, 'isCollapsed');
await row.expand(); await row.expand();
expect(row.detailsRow).toHaveClass('isExpanded'); assertHasClass(row.detailsRow, 'isExpanded');
}); });
it('displays some summary', () => { it('displays some summary', () => {
@@ -202,7 +205,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?balance/i); const summary = await screen.findByTestId(/card-summary:(total )?balance/i);
expect(summary).toHaveTextContent(showBalance(500 + 200 + 150)); assertTextContent(summary, showBalance(500 + 200 + 150));
}); });
it('displays transferable summary', async () => { it('displays transferable summary', async () => {
@@ -213,7 +216,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?transferable/i); const summary = await screen.findByTestId(/card-summary:(total )?transferable/i);
expect(summary).toHaveTextContent(showBalance(400 + 600)); assertTextContent(summary, showBalance(400 + 600));
}); });
it('displays locked summary', async () => { it('displays locked summary', async () => {
@@ -224,7 +227,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?locked/i); const summary = await screen.findByTestId(/card-summary:(total )?locked/i);
expect(summary).toHaveTextContent(showBalance(400 + 600)); assertTextContent(summary, showBalance(400 + 600));
}); });
it('displays bonded summary', async () => { it('displays bonded summary', async () => {
@@ -235,7 +238,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?bonded/i); const summary = await screen.findByTestId(/card-summary:(total )?bonded/i);
expect(summary).toHaveTextContent(showBalance(70 + 20)); assertTextContent(summary, showBalance(70 + 20));
}); });
it('displays unbonding summary', async () => { it('displays unbonding summary', async () => {
@@ -276,7 +279,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?unbonding/i); const summary = await screen.findByTestId(/card-summary:(total )?unbonding/i);
expect(summary).toHaveTextContent(showBalance(200 + 300 + 400 + 100 + 200 + 300)); assertTextContent(summary, showBalance(200 + 300 + 400 + 100 + 200 + 300));
}); });
it('displays redeemable summary', async () => { it('displays redeemable summary', async () => {
@@ -287,7 +290,7 @@ describe.skip('Accounts page', () => {
const summary = await screen.findByTestId(/card-summary:(total )?redeemable/i); const summary = await screen.findByTestId(/card-summary:(total )?redeemable/i);
expect(summary).toHaveTextContent(showBalance(4000 + 5000)); assertTextContent(summary, showBalance(4000 + 5000));
}); });
it('sorts accounts by date by default', async () => { it('sorts accounts by date by default', async () => {
@@ -296,7 +299,7 @@ describe.skip('Accounts page', () => {
anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { whenCreated: 300 }), anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { whenCreated: 300 }),
anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { whenCreated: 100 }) anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { whenCreated: 100 })
); );
expect(await accountsPage.getCurrentSortCategory()).toHaveTextContent('date'); assertTextContent(await accountsPage.getCurrentSortCategory(), 'date');
const accountsTable = await accountsPage.getTable(); const accountsTable = await accountsPage.getTable();
@@ -326,35 +329,29 @@ describe.skip('Accounts page', () => {
it('changes default dropdown value', async () => { it('changes default dropdown value', async () => {
await accountsPage.sortBy('balances'); await accountsPage.sortBy('balances');
expect(await accountsPage.getCurrentSortCategory()) assertTextContent(await accountsPage.getCurrentSortCategory(), 'balances');
.toHaveTextContent('balances');
}); });
// eslint-disable-next-line jest/expect-expect
it('sorts by parent if asked', async () => { it('sorts by parent if asked', async () => {
await accountsPage.sortBy('parent'); await accountsPage.sortBy('parent');
await accountsTable.assertRowsOrder([3, 1, 2]); await accountsTable.assertRowsOrder([3, 1, 2]);
}); });
// eslint-disable-next-line jest/expect-expect
it('sorts by name if asked', async () => { it('sorts by name if asked', async () => {
await accountsPage.sortBy('name'); await accountsPage.sortBy('name');
await accountsTable.assertRowsOrder([3, 2, 1]); await accountsTable.assertRowsOrder([3, 2, 1]);
}); });
// eslint-disable-next-line jest/expect-expect
it('sorts by date if asked', async () => { it('sorts by date if asked', async () => {
await accountsPage.sortBy('date'); await accountsPage.sortBy('date');
await accountsTable.assertRowsOrder([3, 1, 2]); await accountsTable.assertRowsOrder([3, 1, 2]);
}); });
// eslint-disable-next-line jest/expect-expect
it('sorts by balances if asked', async () => { it('sorts by balances if asked', async () => {
await accountsPage.sortBy('balances'); await accountsPage.sortBy('balances');
await accountsTable.assertRowsOrder([1, 2, 3]); await accountsTable.assertRowsOrder([1, 2, 3]);
}); });
// eslint-disable-next-line jest/expect-expect
it('implements stable sort', async () => { it('implements stable sort', async () => {
await accountsPage.sortBy('name'); await accountsPage.sortBy('name');
await accountsTable.assertRowsOrder([3, 2, 1]); await accountsTable.assertRowsOrder([3, 2, 1]);
@@ -362,7 +359,6 @@ describe.skip('Accounts page', () => {
await accountsTable.assertRowsOrder([1, 2, 3]); await accountsTable.assertRowsOrder([1, 2, 3]);
}); });
// eslint-disable-next-line jest/expect-expect
it('respects reverse button', async () => { it('respects reverse button', async () => {
await accountsPage.sortBy('name'); await accountsPage.sortBy('name');
await accountsTable.assertRowsOrder([3, 2, 1]); await accountsTable.assertRowsOrder([3, 2, 1]);
@@ -410,17 +406,14 @@ describe.skip('Accounts page', () => {
await aliceRow.assertAccountName('ALICE'); await aliceRow.assertAccountName('ALICE');
}); });
// eslint-disable-next-line jest/expect-expect
it('the development badge is displayed', async () => { it('the development badge is displayed', async () => {
await aliceRow.assertBadge('wrench-badge'); await aliceRow.assertBadge('wrench-badge');
}); });
// eslint-disable-next-line jest/expect-expect
it('the all networks badge is not displayed', () => { it('the all networks badge is not displayed', () => {
aliceRow.assertNoBadge('exclamation-triangle-badge'); aliceRow.assertNoBadge('exclamation-triangle-badge');
}); });
// eslint-disable-next-line jest/expect-expect
it('the regular badge is not displayed', () => { it('the regular badge is not displayed', () => {
aliceRow.assertNoBadge('transparent-badge'); aliceRow.assertNoBadge('transparent-badge');
}); });
@@ -434,17 +427,14 @@ describe.skip('Accounts page', () => {
await bobRow.assertAccountName('BOB'); await bobRow.assertAccountName('BOB');
}); });
// eslint-disable-next-line jest/expect-expect
it('the development badge is not displayed', () => { it('the development badge is not displayed', () => {
bobRow.assertNoBadge('wrench-badge'); bobRow.assertNoBadge('wrench-badge');
}); });
// eslint-disable-next-line jest/expect-expect
it('the all networks badge is displayed', async () => { it('the all networks badge is displayed', async () => {
await bobRow.assertBadge('exclamation-triangle-badge'); await bobRow.assertBadge('exclamation-triangle-badge');
}); });
// eslint-disable-next-line jest/expect-expect
it('the regular badge is not displayed', () => { it('the regular badge is not displayed', () => {
bobRow.assertNoBadge('transparent-badge'); bobRow.assertNoBadge('transparent-badge');
}); });
@@ -500,7 +490,7 @@ describe.skip('Accounts page', () => {
const modal = await screen.findByTestId('modal'); const modal = await screen.findByTestId('modal');
within(modal).getByText('Pending call hashes'); within(modal).getByText('Pending call hashes');
expect(approvalsModalToggle).toHaveClass('purpleColor'); assertHasClass(approvalsModalToggle, 'purpleColor');
}); });
it('delegate democracy vote', async () => { it('delegate democracy vote', async () => {
@@ -512,7 +502,7 @@ describe.skip('Accounts page', () => {
const modal = await screen.findByTestId('modal'); const modal = await screen.findByTestId('modal');
within(modal).getByText('democracy vote delegation'); within(modal).getByText('democracy vote delegation');
expect(delegateModalToggle).toHaveClass('normalColor'); assertHasClass(delegateModalToggle, 'normalColor');
}); });
it('proxy overview', async () => { it('proxy overview', async () => {
@@ -524,7 +514,7 @@ describe.skip('Accounts page', () => {
const modal = await screen.findByTestId('modal'); const modal = await screen.findByTestId('modal');
within(modal).getByText('Proxy overview'); within(modal).getByText('Proxy overview');
expect(proxyOverviewToggle).toHaveClass('normalColor'); assertHasClass(proxyOverviewToggle, 'normalColor');
}); });
afterEach(() => { afterEach(() => {
@@ -11,7 +11,11 @@ import { assertButtonDisabled, assertText, clickButton, fillInput } from '@pezku
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
import { cryptoWaitReady } from '@pezkuwi/util-crypto'; import { cryptoWaitReady } from '@pezkuwi/util-crypto';
import { AccountsPage } from '../../test/pages/accountsPage.js'; import { mockAccountsPageHooks } from '../../test/mockAccountsPageHooks.js';
await mockAccountsPageHooks();
const { AccountsPage } = await import('../../test/pages/accountsPage.js');
const spy = jest.spyOn(keyring, 'addUri'); const spy = jest.spyOn(keyring, 'addUri');
@@ -19,7 +23,7 @@ const newAccountName = 'NEW ACCOUNT NAME';
const newAccountPassword = 'mySecretPassword'; const newAccountPassword = 'mySecretPassword';
describe('Create an account modal', () => { describe('Create an account modal', () => {
let accountsPage: AccountsPage; let accountsPage: InstanceType<typeof AccountsPage>;
beforeAll(async () => { beforeAll(async () => {
await cryptoWaitReady(); await cryptoWaitReady();
@@ -33,7 +37,6 @@ describe('Create an account modal', () => {
accountsPage = new AccountsPage(); accountsPage = new AccountsPage();
}); });
// eslint-disable-next-line jest/expect-expect
it('creates an account', async () => { it('creates an account', async () => {
await accountsPage.enterCreateAccountModal(); await accountsPage.enterCreateAccountModal();
@@ -53,7 +56,6 @@ describe('Create an account modal', () => {
expectCreateAnAccountCall(); expectCreateAnAccountCall();
}); });
// eslint-disable-next-line jest/expect-expect
it('navigates through the modal flow with enter key', async () => { it('navigates through the modal flow with enter key', async () => {
await accountsPage.enterCreateAccountModal(); await accountsPage.enterCreateAccountModal();
@@ -74,7 +76,6 @@ describe('Create an account modal', () => {
expectCreateAnAccountCall(); expectCreateAnAccountCall();
}); });
// eslint-disable-next-line jest/expect-expect
it('gives an error message when entering invalid derivation path', async () => { it('gives an error message when entering invalid derivation path', async () => {
await accountsPage.enterCreateAccountModal(); await accountsPage.enterCreateAccountModal();
@@ -0,0 +1,19 @@
// Copyright 2017-2026 @pezkuwi/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { mock } from 'node:test';
import { mockPageHooks } from '@pezkuwi/test-support/mocks';
import { mockApiHooks } from '@pezkuwi/test-support/utils';
/**
* The shared page hooks, plus the multisig lookup that an account row makes.
* Await it before importing AccountsPage.
*/
export async function mockAccountsPageHooks (): Promise<void> {
await mockPageHooks();
mock.module('../src/Accounts/useMultisigApprovals', {
defaultExport: () => mockApiHooks.multisigApprovals
});
}
@@ -1,16 +1,15 @@
// Copyright 2017-2026 @pezkuwi/app-accounts authors & contributors // Copyright 2017-2026 @pezkuwi/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/* global expect */
import { within } from '@testing-library/react'; import { within } from '@testing-library/react';
import { Row } from '@pezkuwi/test-support/pagesElements'; import { Row } from '@pezkuwi/test-support/pagesElements';
import { assertTextContent } from '@pezkuwi/test-support/utils';
export class AccountRow extends Row { export class AccountRow extends Row {
async assertParentAccountName (expectedParentAccount: string): Promise<void> { async assertParentAccountName (expectedParentAccount: string): Promise<void> {
const parentAccount = await within(this.primaryRow).findByTestId('parent'); const parentAccount = await within(this.primaryRow).findByTestId('parent');
expect(parentAccount).toHaveTextContent(expectedParentAccount); assertTextContent(parentAccount, expectedParentAccount);
} }
} }
@@ -10,15 +10,19 @@ import { screen } from '@testing-library/react';
import i18next from '@pezkuwi/react-components/i18n'; import i18next from '@pezkuwi/react-components/i18n';
import { aContactWithBalance } from '@pezkuwi/test-support/creation/contact'; import { aContactWithBalance } from '@pezkuwi/test-support/creation/contact';
import { MemoryStore } from '@pezkuwi/test-support/keyring'; import { MemoryStore } from '@pezkuwi/test-support/keyring';
import { balance } from '@pezkuwi/test-support/utils'; import { assertHasClass, balance } from '@pezkuwi/test-support/utils';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
import { AddressesPage } from '../../test/pages/addressesPage.js'; import { mockAddressesPageHooks } from '../../test/mockAddressesPageHooks.js';
await mockAddressesPageHooks();
const { AddressesPage } = await import('../../test/pages/addressesPage.js');
// FIXME isSplit Table // FIXME isSplit Table
// eslint-disable-next-line jest/no-disabled-tests // eslint-disable-next-line jest/no-disabled-tests
describe.skip('Addresses page', () => { describe.skip('Addresses page', () => {
let addressesPage: AddressesPage; let addressesPage: InstanceType<typeof AddressesPage>;
beforeAll(async () => { beforeAll(async () => {
await i18next.changeLanguage('en'); await i18next.changeLanguage('en');
@@ -49,7 +53,6 @@ describe.skip('Addresses page', () => {
expect(await addressesTable.getRows()).toHaveLength(0); expect(await addressesTable.getRows()).toHaveLength(0);
}); });
// eslint-disable-next-line jest/expect-expect
it('the contacts table contains a message about no contacts available', async () => { it('the contacts table contains a message about no contacts available', async () => {
const noContactsMessage = 'no addresses saved yet, add any existing address'; const noContactsMessage = 'no addresses saved yet, add any existing address';
@@ -128,11 +131,11 @@ describe.skip('Addresses page', () => {
addressesPage.renderDefaultContacts(1); addressesPage.renderDefaultContacts(1);
const row = (await addressesPage.getAddressesRows())[0]; const row = (await addressesPage.getAddressesRows())[0];
expect(row.detailsRow).toHaveClass('isCollapsed'); assertHasClass(row.detailsRow, 'isCollapsed');
await row.expand(); await row.expand();
expect(row.detailsRow).toHaveClass('isExpanded'); assertHasClass(row.detailsRow, 'isExpanded');
}); });
}); });
}); });
@@ -0,0 +1,25 @@
// Copyright 2017-2026 @pezkuwi/app-addresses authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { mock } from 'node:test';
import { mockPageHooks } from '@pezkuwi/test-support/mocks';
import { mockAccountHooks } from '@pezkuwi/test-support/utils';
/**
* The shared page hooks, with the contacts served from the same fixture as
* the accounts. Await it before importing AddressesPage.
*/
export async function mockAddressesPageHooks (): Promise<void> {
await mockPageHooks();
mock.module('@pezkuwi/react-hooks/useAddresses', {
namedExports: {
useAddresses: () => ({
allAddresses: mockAccountHooks.useAccounts.allAccounts,
hasAddresses: mockAccountHooks.useAccounts.hasAccounts,
isAddress: true
})
}
});
}
@@ -1,8 +1,6 @@
// Copyright 2017-2026 @pezkuwi/app-addresses authors & contributors // Copyright 2017-2026 @pezkuwi/app-addresses authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/* global jest */
import type { Row } from '@pezkuwi/test-support/pagesElements'; import type { Row } from '@pezkuwi/test-support/pagesElements';
import type { AccountOverrides as ContactOverrides } from '@pezkuwi/test-support/types'; import type { AccountOverrides as ContactOverrides } from '@pezkuwi/test-support/types';
@@ -10,20 +8,11 @@ import React from 'react';
import { aContact } from '@pezkuwi/test-support/creation/contact'; import { aContact } from '@pezkuwi/test-support/creation/contact';
import { Page } from '@pezkuwi/test-support/pages/Page'; import { Page } from '@pezkuwi/test-support/pages/Page';
import { mockAccountHooks } from '@pezkuwi/test-support/utils';
import AddressOverview from '../../src/Contacts/index.js'; import AddressOverview from '../../src/Contacts/index.js';
const NOOP_CHANGE = () => undefined; const NOOP_CHANGE = () => undefined;
jest.mock('@pezkuwi/react-hooks/useAddresses', () => ({
useAddresses: () => ({
allAddresses: mockAccountHooks.useAccounts.allAccounts,
hasAddresses: mockAccountHooks.useAccounts.hasAccounts,
isAddress: true
})
}));
export class AddressesPage extends Page { export class AddressesPage extends Page {
constructor () { constructor () {
super( super(
+30 -24
View File
@@ -3,14 +3,13 @@
/// <reference types="@pezkuwi/dev-test/globals.d.ts" /> /// <reference types="@pezkuwi/dev-test/globals.d.ts" />
/* eslint-disable jest/expect-expect */
import type { SubmittableExtrinsic } from '@pezkuwi/api/types'; import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types'; import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types';
import type { BountyIndex } from '@pezkuwi/types/interfaces'; import type { BountyIndex } from '@pezkuwi/types/interfaces';
import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup'; import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup';
import { fireEvent } from '@testing-library/react'; import { fireEvent } from '@testing-library/react';
import { mock } from 'node:test';
import i18next from '@pezkuwi/react-components/i18n'; import i18next from '@pezkuwi/react-components/i18n';
import { createAugmentedApi } from '@pezkuwi/test-support/api'; import { createAugmentedApi } from '@pezkuwi/test-support/api';
@@ -19,36 +18,43 @@ import { BountyFactory } from '@pezkuwi/test-support/creation/bounties';
import { proposalFactory } from '@pezkuwi/test-support/creation/treasury'; import { proposalFactory } from '@pezkuwi/test-support/creation/treasury';
import { mockHooks } from '@pezkuwi/test-support/hooks'; import { mockHooks } from '@pezkuwi/test-support/hooks';
import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring'; import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring';
import { assertHasClass } from '@pezkuwi/test-support/utils';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
import { BN } from '@pezkuwi/util'; import { BN } from '@pezkuwi/util';
import { defaultBountyUpdatePeriod, mockBountyHooks } from '../test/hooks/defaults.js'; import { defaultBountyUpdatePeriod, mockBountyHooks } from '../test/hooks/defaults.js';
import { BountiesPage } from '../test/pages/bountiesPage.js';
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyNextActionInfo/BountyActionMessage.js';
jest.mock('@pezkuwi/react-hooks/useTreasury', () => ({ // Node only swaps a module that has not been loaded yet, so the page and
useTreasury: () => mockHooks.treasury // anything that loads @pezkuwi/react-hooks are imported after these. A module
})); // mock replaces every export, so each one lists them all.
mock.module('@pezkuwi/react-hooks/useTreasury', {
namedExports: { useTreasury: () => mockHooks.treasury }
});
jest.mock('@pezkuwi/react-hooks/useCollectiveInstance', () => ({ mock.module('@pezkuwi/react-hooks/useCollectiveInstance', {
useCollectiveInstance: () => 'council' namedExports: { useCollectiveInstance: () => 'council' }
})); });
jest.mock('@pezkuwi/react-hooks/useCollectiveMembers', () => ({ mock.module('@pezkuwi/react-hooks/useCollectiveMembers', {
useCollectiveMembers: () => mockHooks.members namedExports: { useCollectiveMembers: () => mockHooks.members }
})); });
jest.mock('@pezkuwi/react-hooks/useBlockTime', () => ({ const { calcBlockTime } = await import('@pezkuwi/react-hooks/useBlockTime');
useBlockTime: () => mockHooks.blockTime
}));
jest.mock('./hooks/useBalance', () => ({ mock.module('@pezkuwi/react-hooks/useBlockTime', {
useBalance: () => mockBountyHooks.balance namedExports: { calcBlockTime, useBlockTime: () => mockHooks.blockTime }
})); });
jest.mock('./hooks/useBounties', () => ({ mock.module('./hooks/useBalance', {
useBounties: () => mockBountyHooks.bountyApi namedExports: { useBalance: () => mockBountyHooks.balance }
})); });
mock.module('./hooks/useBounties', {
namedExports: { useBounties: () => mockBountyHooks.bountyApi }
});
const { BountiesPage } = await import('../test/pages/bountiesPage.js');
const { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } = await import('./BountyNextActionInfo/BountyActionMessage.js');
let aProposal: (extrinsic: SubmittableExtrinsic<'promise'>, ayes?: string[], nays?: string[]) => DeriveCollectiveProposal; let aProposal: (extrinsic: SubmittableExtrinsic<'promise'>, ayes?: string[], nays?: string[]) => DeriveCollectiveProposal;
let augmentedApi: any; let augmentedApi: any;
@@ -58,7 +64,7 @@ let bountyStatusWith: ({ curator, status, updateDue }: { curator?: string, statu
let bountyWith: ({ status, value }: { status?: string, value?: number }) => PezpalletBountiesBounty; let bountyWith: ({ status, value }: { status?: string, value?: number }) => PezpalletBountiesBounty;
describe('Bounties', () => { describe('Bounties', () => {
let bountiesPage: BountiesPage; let bountiesPage: InstanceType<typeof BountiesPage>;
beforeAll(async () => { beforeAll(async () => {
await i18next.changeLanguage('en'); await i18next.changeLanguage('en');
@@ -309,7 +315,7 @@ describe('Bounties', () => {
it('disables Assign Curator button if validation fails', async () => { it('disables Assign Curator button if validation fails', async () => {
await bountiesPage.enterCuratorsFee('6'); await bountiesPage.enterCuratorsFee('6');
expect(await bountiesPage.assignCuratorButton()).toHaveClass('isDisabled'); assertHasClass(await bountiesPage.assignCuratorButton(), 'isDisabled');
}); });
it('queues propose extrinsic on submit', async () => { it('queues propose extrinsic on submit', async () => {
@@ -12,7 +12,7 @@ import type { BountyIndex } from '@pezkuwi/types/interfaces';
import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup'; import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup';
import type { BountyApi } from '../../src/hooks/index.js'; import type { BountyApi } from '../../src/hooks/index.js';
import { fireEvent, render, within } from '@testing-library/react'; import { fireEvent, render, waitFor, within } from '@testing-library/react';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
@@ -24,6 +24,7 @@ import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue'; import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue';
import { balanceOf } from '@pezkuwi/test-support/creation/balance'; import { balanceOf } from '@pezkuwi/test-support/creation/balance';
import { BountyFactory } from '@pezkuwi/test-support/creation/bounties'; import { BountyFactory } from '@pezkuwi/test-support/creation/bounties';
import { assertVisible } from '@pezkuwi/test-support/utils';
import { TypeRegistry } from '@pezkuwi/types/create'; import { TypeRegistry } from '@pezkuwi/types/create';
import Bounties from '../../src/Bounties.js'; import Bounties from '../../src/Bounties.js';
@@ -330,6 +331,10 @@ export class BountiesPage {
const icon = await within(votingInfo).findByTestId('question-circle'); const icon = await within(votingInfo).findByTestId('question-circle');
fireEvent.mouseEnter(icon); fireEvent.mouseEnter(icon);
expect(await this.findByText(description)).toBeVisible();
const text = await this.findByText(description);
// the tooltip renders hidden, then fades in
await waitFor(() => assertVisible(text));
} }
} }
@@ -12,16 +12,20 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import i18next from '@pezkuwi/react-components/i18n'; import i18next from '@pezkuwi/react-components/i18n';
import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@pezkuwi/test-support/creation/account'; import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@pezkuwi/test-support/creation/account';
import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring'; import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring';
import { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } from '@pezkuwi/test-support/mockData'; import { assertHasClass, assertTextContent, mockApiHooks } from '@pezkuwi/test-support/utils';
import { mockApiHooks } from '@pezkuwi/test-support/utils';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
import { AccountsPage } from '../../../page-accounts/test/pages/accountsPage.js'; import { mockAccountsPageHooks } from '../../../page-accounts/test/mockAccountsPageHooks.js';
await mockAccountsPageHooks();
const { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } = await import('@pezkuwi/test-support/mockData');
const { AccountsPage } = await import('../../../page-accounts/test/pages/accountsPage.js');
// FIXME: these all need to be wrapped in waitFor .... // FIXME: these all need to be wrapped in waitFor ....
// eslint-disable-next-line jest/no-disabled-tests // eslint-disable-next-line jest/no-disabled-tests
describe.skip('Sidebar', () => { describe.skip('Sidebar', () => {
let accountsPage: AccountsPage; let accountsPage: InstanceType<typeof AccountsPage>;
let sideBar: Sidebar; let sideBar: Sidebar;
beforeAll(async () => { beforeAll(async () => {
@@ -62,7 +66,6 @@ describe.skip('Sidebar', () => {
expect(changedAccount?.meta?.name).toEqual(newName); expect(changedAccount?.meta?.name).toEqual(newName);
}); });
// eslint-disable-next-line jest/expect-expect
it('within sidebar', async () => { it('within sidebar', async () => {
await sideBar.assertAccountName(newName); await sideBar.assertAccountName(newName);
}); });
@@ -105,7 +108,6 @@ describe.skip('Sidebar', () => {
sideBar.edit(); sideBar.edit();
}); });
// eslint-disable-next-line jest/expect-expect
it('restores tags and name to state from keyring', async () => { it('restores tags and name to state from keyring', async () => {
await sideBar.typeAccountName(newName); await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag); await sideBar.selectTag(defaultTag);
@@ -149,7 +151,6 @@ describe.skip('Sidebar', () => {
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeTruthy(); expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeTruthy();
}); });
// eslint-disable-next-line jest/expect-expect
it('cancels editing and changes name when opening sidebar for another account', async () => { it('cancels editing and changes name when opening sidebar for another account', async () => {
await waitFor(() => sideBar.assertAccountInput('alice')); await waitFor(() => sideBar.assertAccountInput('alice'));
@@ -188,12 +189,11 @@ describe.skip('Sidebar', () => {
const subsNumber = subs[0].childNodes[0]; const subsNumber = subs[0].childNodes[0];
const subAccount = subs[0].childNodes[1]; const subAccount = subs[0].childNodes[1];
expect(subsNumber).toHaveClass('subs-number'); assertHasClass(subsNumber, 'subs-number');
expect(subsNumber).toHaveTextContent('1'); assertTextContent(subsNumber, '1');
expect(subAccount).toHaveTextContent('BOB'); assertTextContent(subAccount, 'BOB');
}); });
// eslint-disable-next-line jest/expect-expect
it('displays picked sub in sidebar', async () => { it('displays picked sub in sidebar', async () => {
const subAccount = subs[0].childNodes[1]; const subAccount = subs[0].childNodes[1];
@@ -205,7 +205,6 @@ describe.skip('Sidebar', () => {
}); });
describe('judgements', () => { describe('judgements', () => {
// eslint-disable-next-line jest/expect-expect
it('displays several judgements', async () => { it('displays several judgements', async () => {
mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]); mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]);
accountsPage.renderDefaultAccounts(1); accountsPage.renderDefaultAccounts(1);
@@ -216,7 +215,6 @@ describe.skip('Sidebar', () => {
await sideBar.assertJudgement('1 Erroneous'); await sideBar.assertJudgement('1 Erroneous');
}); });
// eslint-disable-next-line jest/expect-expect
it('displays no judgements', async () => { it('displays no judgements', async () => {
accountsPage.renderDefaultAccounts(1); accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0); sideBar = await accountsPage.openSidebarForRow(0);
@@ -235,14 +233,12 @@ describe.skip('Sidebar', () => {
sideBar = await accountsPage.openSidebarForRow(0); sideBar = await accountsPage.openSidebarForRow(0);
}); });
// eslint-disable-next-line jest/expect-expect
it('singular registrar', async () => { it('singular registrar', async () => {
const judgementTag = await sideBar.getJudgement('1 Known good'); const judgementTag = await sideBar.getJudgement('1 Known good');
await judgementTag.assertRegistrars([charlieShortAddress]); await judgementTag.assertRegistrars([charlieShortAddress]);
}); });
// eslint-disable-next-line jest/expect-expect
it('multiple registrars', async () => { it('multiple registrars', async () => {
const judgementTag = await sideBar.getJudgement('2 Reasonable'); const judgementTag = await sideBar.getJudgement('2 Reasonable');
+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 { RenderResult } from '@testing-library/react';
import type { ApiProps } from '@pezkuwi/react-api/types'; import type { ApiProps } from '@pezkuwi/react-api/types';
import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@pezkuwi/react-components/Status/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 type { AccountOverrides } from '../utils/accountDefaults.js';
import { queryByAttribute, render, screen } from '@testing-library/react'; 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 { alice, bob, charlie, ferdie } from '../keyring/index.js';
import { Table } from '../pagesElements/index.js'; import { Table } from '../pagesElements/index.js';
import { mockAccountHooks } from '../utils/accountDefaults.js'; import { mockAccountHooks } from '../utils/accountDefaults.js';
import { mockApiHooks } from '../utils/mockApiHooks.js';
let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void; let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
class NotYetRendered extends Error { 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 { export abstract class Page {
private renderResult?: RenderResult; private renderResult?: RenderResult;
protected readonly defaultAddresses = [alice, bob, charlie, ferdie]; protected readonly defaultAddresses = [alice, bob, charlie, ferdie];
@@ -198,6 +115,8 @@ export abstract class Page {
}; };
const mockApi: ApiProps = { const mockApi: ApiProps = {
api, api,
// identities live on the people chain; here both are the same mock
apiIdentity: api,
apiSystem: { apiSystem: {
...api, ...api,
isReady: Promise.resolve(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 { fireEvent, screen, within } from '@testing-library/react';
import { format } from '../utils/balance.js'; import { format } from '../utils/balance.js';
import { assertTextContent } from '../utils/domAssertions.js';
import { Sidebar } from './Sidebar.js'; import { Sidebar } from './Sidebar.js';
// utility wrapper over an account item in accounts table, serves basic assertions about an account row // 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 actualBalanceText = await this.getBalanceSummary();
const expectedBalanceText = format(expectedTotalBalance); const expectedBalanceText = format(expectedTotalBalance);
expect(actualBalanceText).toHaveTextContent(expectedBalanceText); assertTextContent(actualBalanceText, expectedBalanceText);
} }
async getBalanceSummary (): Promise<HTMLElement> { async getBalanceSummary (): Promise<HTMLElement> {
@@ -34,7 +35,7 @@ export class Row {
async assertAccountName (expectedName: string): Promise<void> { async assertAccountName (expectedName: string): Promise<void> {
const accountName = await this.getAccountName(); const accountName = await this.getAccountName();
expect(accountName).toHaveTextContent(expectedName); assertTextContent(accountName, expectedName);
} }
async assertBalancesDetails (expectedBalanceComponents: { name: string, amount: Balance }[]): Promise<void> { async assertBalancesDetails (expectedBalanceComponents: { name: string, amount: Balance }[]): Promise<void> {
@@ -54,13 +55,13 @@ export class Row {
async assertTags (expectedTagsContent: string): Promise<void> { async assertTags (expectedTagsContent: string): Promise<void> {
const actualTags = await within(this.detailsRow).findByTestId('tags'); const actualTags = await within(this.detailsRow).findByTestId('tags');
expect(actualTags).toHaveTextContent(expectedTagsContent); assertTextContent(actualTags, expectedTagsContent);
} }
async assertShortAddress (expectedShortAddress: string): Promise<void> { async assertShortAddress (expectedShortAddress: string): Promise<void> {
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address'); const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
expect(actualShortAddress).toHaveTextContent(expectedShortAddress); assertTextContent(actualShortAddress, expectedShortAddress);
} }
async expand (): Promise<void> { async expand (): Promise<void> {
@@ -85,7 +86,7 @@ export class Row {
const balanceElement = await this.getBalanceElementByLabelName(expectedBalanceComponent.name); const balanceElement = await this.getBalanceElementByLabelName(expectedBalanceComponent.name);
const balanceText = format(expectedBalanceComponent.amount); const balanceText = format(expectedBalanceComponent.amount);
expect(balanceElement).toHaveTextContent(balanceText); assertTextContent(balanceElement, balanceText);
} }
private async getBalanceElementByLabelName (labelName: string): Promise<ChildNode | null> { private async getBalanceElementByLabelName (labelName: string): Promise<ChildNode | null> {
@@ -5,6 +5,7 @@
import { fireEvent, screen, within } from '@testing-library/react'; import { fireEvent, screen, within } from '@testing-library/react';
import { assertTextContent } from '../utils/domAssertions.js';
import { JudgementTag } from './JudgementTag.js'; import { JudgementTag } from './JudgementTag.js';
export class Sidebar { export class Sidebar {
@@ -48,19 +49,19 @@ export class Sidebar {
const sideBarAddressSection = await this.findByTestId('sidebar-address-menu'); const sideBarAddressSection = await this.findByTestId('sidebar-address-menu');
const sideBarName = await within(sideBarAddressSection).findByTestId('account-name'); const sideBarName = await within(sideBarAddressSection).findByTestId('account-name');
expect(sideBarName).toHaveTextContent(expectedAccountName); assertTextContent(sideBarName, expectedAccountName);
} }
async assertJudgement (judgement: string): Promise<void> { async assertJudgement (judgement: string): Promise<void> {
const judgementsSection = await this.findByTestId('judgements'); const judgementsSection = await this.findByTestId('judgements');
expect(judgementsSection).toHaveTextContent(judgement); assertTextContent(judgementsSection, judgement);
} }
async assertTags (tagsContent: string): Promise<void> { async assertTags (tagsContent: string): Promise<void> {
const sideBarTags = await this.findByTestId('sidebar-tags'); const sideBarTags = await this.findByTestId('sidebar-tags');
expect(sideBarTags).toHaveTextContent(tagsContent); assertTextContent(sideBarTags, tagsContent);
} }
close (): Promise<void> { close (): Promise<void> {
@@ -6,6 +6,7 @@
import { within } from '@testing-library/react'; import { within } from '@testing-library/react';
import { showBalance } from '../utils/balance.js'; import { showBalance } from '../utils/balance.js';
import { assertTextContent } from '../utils/domAssertions.js';
import { Row } from './Row.js'; import { Row } from './Row.js';
export class Table { export class Table {
@@ -21,7 +22,7 @@ export class Table {
const row = orderedRows[index]; const row = orderedRows[index];
const expectedBalanceTextContent = showBalance(balancesExpectedOrder[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 './accountDefaults.js';
export * from './balance.js'; export * from './balance.js';
export * from './domAssertions.js';
export * from './mockApiHooks.js'; export * from './mockApiHooks.js';
export * from './renderedScreenUtils.js'; export * from './renderedScreenUtils.js';
export * from './waitFor.js'; export * from './waitFor.js';
@@ -1,10 +1,10 @@
// Copyright 2017-2026 @pezkuwi/test-supports authors & contributors // Copyright 2017-2026 @pezkuwi/test-supports authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/* global expect */
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen } from '@testing-library/react';
import { assertHasClass } from './domAssertions.js';
export const clickButton = async (buttonName: string): Promise<void> => { export const clickButton = async (buttonName: string): Promise<void> => {
const button = await screen.findByRole('button', { name: buttonName }); 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 => { export const assertButtonDisabled = (buttonName: string): void => {
const button = screen.getByRole('button', { name: buttonName }); const button = screen.getByRole('button', { name: buttonName });
expect(button).toHaveClass('isDisabled'); assertHasClass(button, 'isDisabled');
}; };
+1
View File
@@ -127,6 +127,7 @@
"@pezkuwi/test-support/hooks": ["test-support/src/hooks/index.ts"], "@pezkuwi/test-support/hooks": ["test-support/src/hooks/index.ts"],
"@pezkuwi/test-support/keyring": ["test-support/src/keyring/index.ts"], "@pezkuwi/test-support/keyring": ["test-support/src/keyring/index.ts"],
"@pezkuwi/test-support/mockData": ["test-support/src/mockData/index.ts"], "@pezkuwi/test-support/mockData": ["test-support/src/mockData/index.ts"],
"@pezkuwi/test-support/mocks": ["test-support/src/mocks/index.ts"],
"@pezkuwi/test-support/pages/*": ["test-support/src/pages/*.tsx"], "@pezkuwi/test-support/pages/*": ["test-support/src/pages/*.tsx"],
"@pezkuwi/test-support/pagesElements": ["test-support/src/pagesElements/index.ts"], "@pezkuwi/test-support/pagesElements": ["test-support/src/pagesElements/index.ts"],
"@pezkuwi/test-support/react": ["test-support/src/react/index.ts"], "@pezkuwi/test-support/react": ["test-support/src/react/index.ts"],