Accounts, Contacts and Sidebar specs: follow the current table layout, run them

The three suites were skipped since the table rework. What changed under them:

- An account or contact spans three rows (main, total balance, details),
  marked from isFirst; the helpers paired rows by two and filtered on a
  row class the rows no longer carry. A collapsed details row is hidden,
  so rows are read including hidden ones.
- Accounts render one table per account group; the page object reads the
  rows of every group table in order, and the sorting fixture keeps its
  three accounts in one group, since sorting applies within a group.
- The expand toggle is the row's expand column, not a row-toggle element.
- useAccountInfo reads ownership from the keyring context, which the test
  page did not provide, so saving a name did nothing; the page now renders
  the real KeyringCtxRoot. The mock api gains consts.system.ss58Prefix.
- Badge popups (react-tooltip v6) render only while hovered; the specs
  hover the badge first. The multisig badge needs the isMultisig flag.
- Addresses are shown in full and shortened by css; the fixtures with
  shortened addresses are gone.
- The total balance, transferable and locked cards are always shown, as
  placeholders without accounts; the specs check that instead of their
  absence.
- Checks that needed matchers the runner lacks (not.toHaveLength,
  toHaveProperty, toThrow with a contained message) are written against
  the DOM directly.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-10 14:12:47 +04:00
1 parent 61c681567d
commit 5380b54ca6
11 files changed
+136 -107

No files matched your search

@@ -9,15 +9,14 @@ import type { u32 } from '@pezkuwi/types';
import type { AccountId, Multisig, ProxyDefinition, Timepoint, Voting, VotingDelegating } from '@pezkuwi/types/interfaces'; import type { AccountId, Multisig, ProxyDefinition, Timepoint, Voting, VotingDelegating } from '@pezkuwi/types/interfaces';
import type { AccountRow } from '../../test/pageElements/AccountRow.js'; import type { AccountRow } from '../../test/pageElements/AccountRow.js';
import { fireEvent, screen, within } from '@testing-library/react'; import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config'; import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config';
import i18next from '@pezkuwi/react-components/i18n'; import i18next from '@pezkuwi/react-components/i18n';
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 { assertHasClass, assertTextContent, balance, mockApiHooks, showBalance } from '@pezkuwi/test-support/utils'; import { assertHasClass, assertNotHasClass, 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';
@@ -28,9 +27,10 @@ await mockAccountsPageHooks();
const { AccountsPage } = await import('../../test/pages/accountsPage.js'); const { AccountsPage } = await import('../../test/pages/accountsPage.js');
// FIXME isSplit Table // the totals always shown; bonded, redeemable and unbonding only when non-zero
// eslint-disable-next-line jest/no-disabled-tests const SUMMARY_TOTALS = ['card-summary:total balance', 'card-summary:total transferable', 'card-summary:total locked'];
describe.skip('Accounts page', () => {
describe('Accounts page', () => {
let accountsPage: InstanceType<typeof AccountsPage>; let accountsPage: InstanceType<typeof AccountsPage>;
beforeAll(async () => { beforeAll(async () => {
@@ -75,10 +75,11 @@ describe.skip('Accounts page', () => {
await accountsTable.assertText(noAccountsMessage); await accountsTable.assertText(noAccountsMessage);
}); });
it('no summary is displayed', () => { it('the summary shows placeholders, and no staking totals', async () => {
const summaries = screen.queryAllByTestId(/card-summary:total \w+/i); const summaries = await screen.findAllByTestId(/card-summary:/i);
expect(summaries).toHaveLength(0); expect(summaries.map((s) => s.dataset.testid)).toEqual(SUMMARY_TOTALS);
summaries.forEach((s) => assertHasClass(s.querySelector('.ui--FormatBalance'), '--tmp'));
}); });
}); });
@@ -120,9 +121,7 @@ describe.skip('Accounts page', () => {
{ amount: balance(150), name: 'reserved' }]); { amount: balance(150), name: 'reserved' }]);
}); });
// FIXME multiple tables it('derived account displays parent account info', async () => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('derived account displays parent account info', async () => {
accountsPage.renderAccountsWithDefaultAddresses( accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ isInjected: true, name: 'ALICE', whenCreated: 200 }), anAccountWithMeta({ isInjected: true, name: 'ALICE', whenCreated: 200 }),
anAccountWithMeta({ name: 'ALICE_CHILD', parentAddress: alice, whenCreated: 300 }) anAccountWithMeta({ name: 'ALICE_CHILD', parentAddress: alice, whenCreated: 300 })
@@ -134,26 +133,25 @@ describe.skip('Accounts page', () => {
await accountRows[1].assertParentAccountName('ALICE'); await accountRows[1].assertParentAccountName('ALICE');
}); });
// FIXME broken after column rework it('parent and type are not separate columns, the type is in the row details', async () => {
// eslint-disable-next-line jest/no-disabled-tests
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();
const rows = await accountsTable.getRows();
accountsTable.assertColumnNotExist('parent'); accountsTable.assertColumnNotExist('parent');
accountsTable.assertColumnExists('type'); accountsTable.assertColumnNotExist('type');
assertTextContent(rows[0].detailsRow, /account type ?qr/);
}); });
it('account rows display the shorted address', async () => { it('account rows display the address', async () => {
accountsPage.renderAccountsForAddresses( accountsPage.renderAccountsForAddresses(
alice alice
); );
const accountRows = await accountsPage.getAccountRows(); const accountRows = await accountsPage.getAccountRows();
expect(accountRows).toHaveLength(1); expect(accountRows).toHaveLength(1);
const aliceShortAddress = toShortAddress(alice); // shown in full, shortened by css
await accountRows[0].assertShortAddress(alice);
await accountRows[0].assertShortAddress(aliceShortAddress);
}); });
// eslint-disable-next-line jest/expect-expect // eslint-disable-next-line jest/expect-expect
@@ -181,20 +179,23 @@ describe.skip('Accounts page', () => {
assertHasClass(row.detailsRow, 'isCollapsed'); assertHasClass(row.detailsRow, 'isCollapsed');
await row.expand(); row.expand();
assertHasClass(row.detailsRow, 'isExpanded'); assertHasClass(row.detailsRow, 'isExpanded');
}); });
it('displays some summary', () => { it('displays the summary totals, not placeholders', async () => {
accountsPage.renderAccountsWithDefaultAddresses( accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithBalance({ freeBalance: balance(500) }), anAccountWithBalance({ freeBalance: balance(500) }),
anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) }) anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
); );
const summaries = screen.queryAllByTestId(/card-summary:total \w+/i); await waitFor(async () => {
const summaries = await screen.findAllByTestId(/card-summary:total \w+/i);
expect(summaries).not.toHaveLength(0); expect(summaries.map((s) => s.dataset.testid)).toEqual(SUMMARY_TOTALS);
summaries.forEach((s) => assertNotHasClass(s.querySelector('.ui--FormatBalance'), '--tmp'));
});
}); });
it('displays balance summary', async () => { it('displays balance summary', async () => {
@@ -306,21 +307,14 @@ describe.skip('Accounts page', () => {
await accountsTable.assertRowsOrder([3, 1, 2]); await accountsTable.assertRowsOrder([3, 1, 2]);
}); });
// FIXME multiple tables now // the page sorts within each account group, so all three share a group
// eslint-disable-next-line jest/no-disabled-tests describe('when sorting is used', () => {
describe.skip('when sorting is used', () => {
let accountsTable: Table; let accountsTable: Table;
beforeEach(async () => { beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses( accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithBalanceAndMeta({ freeBalance: balance(1) }, { isInjected: true, name: 'bbb', whenCreated: 200 }), anAccountWithBalanceAndMeta({ freeBalance: balance(1) }, { isInjected: true, name: 'bbb', whenCreated: 200 }),
anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { isInjected: true, name: 'bb', parentAddress: alice, whenCreated: 300 }),
hardwareType: 'ledger',
isHardware: true,
name: 'bb',
parentAddress: alice,
whenCreated: 300
}),
anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { isInjected: true, name: 'aaa', whenCreated: 100 }) anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { isInjected: true, name: 'aaa', whenCreated: 100 })
); );
@@ -468,22 +462,20 @@ describe.skip('Accounts page', () => {
describe('show popups', () => { describe('show popups', () => {
beforeEach(async () => { beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses( accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { name: 'alice', who: [] }) anAccountWithInfoAndMeta({ flags: { isDevelopment: true, isMultisig: true } as AddressFlags }, { name: 'alice', who: [] })
); );
accountRows = await accountsPage.getAccountRows(); accountRows = await accountsPage.getAccountRows();
}); });
// eslint-disable-next-line jest/expect-expect // eslint-disable-next-line jest/expect-expect
it('development', async () => { it('development', async () => {
await accountRows[0].assertBadge('wrench-badge'); const badgePopup = await openBadgePopup('wrench-badge');
const badgePopup = getPopupById(/wrench-badge-hover.*/);
await within(badgePopup).findByText('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.'); await within(badgePopup).findByText('This is a development account derived from the known development seed. Do not use for any funds on a non-development network.');
}); });
it('multisig approvals', async () => { it('multisig approvals', async () => {
await accountRows[0].assertBadge('file-signature-badge'); const badgePopup = await openBadgePopup('file-signature-badge');
const badgePopup = getPopupById(/file-signature-badge-hover.*/);
const approvalsModalToggle = await within(badgePopup).findByText('View pending approvals'); const approvalsModalToggle = await within(badgePopup).findByText('View pending approvals');
fireEvent.click(approvalsModalToggle); fireEvent.click(approvalsModalToggle);
@@ -494,8 +486,7 @@ describe.skip('Accounts page', () => {
}); });
it('delegate democracy vote', async () => { it('delegate democracy vote', async () => {
await accountRows[0].assertBadge('calendar-check-badge'); const badgePopup = await openBadgePopup('calendar-check-badge');
const badgePopup = getPopupById(/calendar-check-badge-hover.*/);
const delegateModalToggle = await within(badgePopup).findByText('Manage delegation'); const delegateModalToggle = await within(badgePopup).findByText('Manage delegation');
fireEvent.click(delegateModalToggle); fireEvent.click(delegateModalToggle);
@@ -506,8 +497,7 @@ describe.skip('Accounts page', () => {
}); });
it('proxy overview', async () => { it('proxy overview', async () => {
await accountRows[0].assertBadge('sitemap-badge'); const badgePopup = await openBadgePopup('sitemap-badge');
const badgePopup = getPopupById(/sitemap-badge-hover.*/);
const proxyOverviewToggle = await within(badgePopup).findByText('Manage proxies'); const proxyOverviewToggle = await within(badgePopup).findByText('Manage proxies');
fireEvent.click(proxyOverviewToggle); fireEvent.click(proxyOverviewToggle);
@@ -522,14 +512,21 @@ describe.skip('Accounts page', () => {
}); });
}); });
function getPopupById (popupId: RegExp): HTMLElement { // a badge renders its popup while hovered
const badgePopup = accountsPage.getById(popupId); async function openBadgePopup (badgeTestId: string): Promise<HTMLElement> {
const badge = await within(accountRows[0].primaryRow).findByTestId(badgeTestId);
if (!badgePopup) { fireEvent.mouseEnter(badge);
throw new Error('badge popup should be found');
}
return badgePopup; return waitFor(() => {
const badgePopup = accountsPage.getById(new RegExp(`^${badgeTestId}-hover-`));
if (!badgePopup) {
throw new Error(`${badgeTestId} popup should be found`);
}
return badgePopup;
});
} }
}); });
}); });
@@ -23,16 +23,21 @@ settings.set({ ...settings.get(), storage: 'on' });
export class AccountsPage extends Page { export class AccountsPage extends Page {
constructor () { constructor () {
super( super(
<AccountOverview onStatusChange={NOOP_CHANGE} />, <AccountOverview onStatusChange={NOOP_CHANGE} />
'Account-'
); );
} }
/** The rows of every account group table, in the order shown */
async getAccountRows (): Promise<AccountRow[]> { async getAccountRows (): Promise<AccountRow[]> {
const table = await this.getTable(); const rows: AccountRow[] = [];
const rows = await table.getRows();
return rows.map((row) => new AccountRow(row.primaryRow, row.detailsRow)); for (const table of await this.getTables()) {
for (const row of await table.getRows()) {
rows.push(new AccountRow(row.primaryRow, row.balanceRow, row.detailsRow));
}
}
return rows;
} }
async reverseSortingOrder (): Promise<void> { async reverseSortingOrder (): Promise<void> {
@@ -19,9 +19,7 @@ await mockAddressesPageHooks();
const { AddressesPage } = await import('../../test/pages/addressesPage.js'); const { AddressesPage } = await import('../../test/pages/addressesPage.js');
// FIXME isSplit Table describe('Addresses page', () => {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Addresses page', () => {
let addressesPage: InstanceType<typeof AddressesPage>; let addressesPage: InstanceType<typeof AddressesPage>;
beforeAll(async () => { beforeAll(async () => {
@@ -133,7 +131,7 @@ describe.skip('Addresses page', () => {
assertHasClass(row.detailsRow, 'isCollapsed'); assertHasClass(row.detailsRow, 'isCollapsed');
await row.expand(); row.expand();
assertHasClass(row.detailsRow, 'isExpanded'); assertHasClass(row.detailsRow, 'isExpanded');
}); });
@@ -16,8 +16,7 @@ const NOOP_CHANGE = () => undefined;
export class AddressesPage extends Page { export class AddressesPage extends Page {
constructor () { constructor () {
super( super(
<AddressOverview onStatusChange={NOOP_CHANGE} />, <AddressOverview onStatusChange={NOOP_CHANGE} />
'Address-'
); );
} }
@@ -11,7 +11,7 @@ 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, charlie, ferdie, MemoryStore } from '@pezkuwi/test-support/keyring';
import { assertHasClass, assertTextContent, mockApiHooks } from '@pezkuwi/test-support/utils'; import { assertHasClass, assertTextContent, mockApiHooks } from '@pezkuwi/test-support/utils';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
@@ -19,12 +19,10 @@ import { mockAccountsPageHooks } from '../../../page-accounts/test/mockAccountsP
await mockAccountsPageHooks(); await mockAccountsPageHooks();
const { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } = await import('@pezkuwi/test-support/mockData'); const { mockRegistration, registrars } = await import('@pezkuwi/test-support/mockData');
const { AccountsPage } = await import('../../../page-accounts/test/pages/accountsPage.js'); const { AccountsPage } = await import('../../../page-accounts/test/pages/accountsPage.js');
// FIXME: these all need to be wrapped in waitFor .... describe('Sidebar', () => {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Sidebar', () => {
let accountsPage: InstanceType<typeof AccountsPage>; let accountsPage: InstanceType<typeof AccountsPage>;
let sideBar: Sidebar; let sideBar: Sidebar;
@@ -45,7 +43,6 @@ describe.skip('Sidebar', () => {
const initialName = 'INITIAL_NAME'; const initialName = 'INITIAL_NAME';
const newName = 'NEW_NAME'; const newName = 'NEW_NAME';
const defaultTag = 'Default'; const defaultTag = 'Default';
const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]';
describe('changes name', () => { describe('changes name', () => {
beforeEach(async () => { beforeEach(async () => {
@@ -83,8 +80,7 @@ describe.skip('Sidebar', () => {
sideBar = await accountsPage.openSidebarForRow(0); sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.clickByText('none'); await sideBar.clickByText('none');
expect(sideBar.queryByRole('combobox')).toBeFalsy(); expect(sideBar.queryByRole('combobox')).toBeFalsy();
expect(sideBar.queryByTestId('name-input')).toBeFalsy();
await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError);
}); });
it('when isEditable is false account name is not editable', async () => { it('when isEditable is false account name is not editable', async () => {
@@ -94,7 +90,7 @@ describe.skip('Sidebar', () => {
sideBar = await accountsPage.openSidebarForRow(0); sideBar = await accountsPage.openSidebarForRow(0);
sideBar.edit(); sideBar.edit();
await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError); expect(sideBar.queryByTestId('name-input')).toBeFalsy();
}); });
describe('on edit cancel', () => { describe('on edit cancel', () => {
@@ -137,7 +133,7 @@ describe.skip('Sidebar', () => {
await sideBar.typeAccountName(newName); await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag); await sideBar.selectTag(defaultTag);
fireEvent.click(await screen.findByText('accounts')); fireEvent.click(await screen.findByTestId('sort-by-section'));
await sideBar.assertTags('none'); await sideBar.assertTags('none');
await sideBar.assertAccountName('ALICE'); await sideBar.assertAccountName('ALICE');
@@ -236,13 +232,13 @@ describe.skip('Sidebar', () => {
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([charlie]);
}); });
it('multiple registrars', async () => { it('multiple registrars', async () => {
const judgementTag = await sideBar.getJudgement('2 Reasonable'); const judgementTag = await sideBar.getJudgement('2 Reasonable');
await judgementTag.assertRegistrars(['BOB', ferdieShortAddress]); await judgementTag.assertRegistrars(['BOB', ferdie]);
}); });
it('opens clicked registrar in sidebar and closes popup', async () => { it('opens clicked registrar in sidebar and closes popup', async () => {
@@ -41,7 +41,3 @@ export const charlieRegistrar: Registrar = { address: charlie, index: 1 };
export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 }; export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 };
export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar]; export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar];
export const bobShortAddress = '5FHneW…M694ty';
export const charlieShortAddress = '5DAAnr…3PTXFy';
export const ferdieShortAddress = '5CiPPs…SK2DjL';
+18 -6
View File
@@ -16,6 +16,7 @@ import { ThemeProvider } from 'styled-components';
import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config'; import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config';
import { AccountSidebar, lightTheme } from '@pezkuwi/react-components'; import { AccountSidebar, lightTheme } from '@pezkuwi/react-components';
import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api'; import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
import { KeyringCtxRoot } from '@pezkuwi/react-hooks/ctx/Keyring';
import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue'; import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue';
import { TypeRegistry } from '@pezkuwi/types/create'; import { TypeRegistry } from '@pezkuwi/types/create';
import { keyring } from '@pezkuwi/ui-keyring'; import { keyring } from '@pezkuwi/ui-keyring';
@@ -34,9 +35,8 @@ export abstract class Page {
private renderResult?: RenderResult; private renderResult?: RenderResult;
protected readonly defaultAddresses = [alice, bob, charlie, ferdie]; protected readonly defaultAddresses = [alice, bob, charlie, ferdie];
protected constructor (private readonly overview: React.ReactElement, private readonly rowClassName: string) { protected constructor (private readonly overview: React.ReactElement) {
this.overview = overview; this.overview = overview;
this.rowClassName = rowClassName;
} }
render (accounts: [string, AccountOverrides][]): void { render (accounts: [string, AccountOverrides][]): void {
@@ -59,6 +59,9 @@ export abstract class Page {
proxy: { proxy: {
proxyDepositBase: new BN(1), proxyDepositBase: new BN(1),
proxyDepositFactor: new BN(1) proxyDepositFactor: new BN(1)
},
system: {
ss58Prefix: new BN(42)
} }
}, },
createType: () => ({ createType: () => ({
@@ -141,9 +144,11 @@ export abstract class Page {
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<ApiCtx.Provider value={mockApi}> <ApiCtx.Provider value={mockApi}>
<AccountSidebar> <KeyringCtxRoot>
{React.cloneElement(this.overview, { onStatusChange: noop }) } <AccountSidebar>
</AccountSidebar> {React.cloneElement(this.overview, { onStatusChange: noop }) }
</AccountSidebar>
</KeyringCtxRoot>
</ApiCtx.Provider> </ApiCtx.Provider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
@@ -156,7 +161,14 @@ export abstract class Page {
async getTable (): Promise<Table> { async getTable (): Promise<Table> {
this.assertRendered(); this.assertRendered();
return new Table(await screen.findByRole('table'), this.rowClassName); return new Table(await screen.findByRole('table'));
}
/** All tables, in document order, for a page that splits its items over several */
async getTables (): Promise<Table[]> {
this.assertRendered();
return (await screen.findAllByRole('table')).map((table) => new Table(table));
} }
clearAccounts (): void { clearAccounts (): void {
+11 -4
View File
@@ -14,10 +14,12 @@ 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
export class Row { export class Row {
public primaryRow: HTMLElement; public primaryRow: HTMLElement;
public balanceRow: HTMLElement;
public detailsRow: HTMLElement; public detailsRow: HTMLElement;
constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) { constructor (primaryRow: HTMLElement, balanceRow: HTMLElement, detailsRow: HTMLElement) {
this.primaryRow = primaryRow; this.primaryRow = primaryRow;
this.balanceRow = balanceRow;
this.detailsRow = detailsRow; this.detailsRow = detailsRow;
} }
@@ -29,7 +31,7 @@ export class Row {
} }
async getBalanceSummary (): Promise<HTMLElement> { async getBalanceSummary (): Promise<HTMLElement> {
return within(this.primaryRow).findByTestId('balance-summary'); return within(this.balanceRow).findByTestId('balance-summary');
} }
async assertAccountName (expectedName: string): Promise<void> { async assertAccountName (expectedName: string): Promise<void> {
@@ -64,8 +66,13 @@ export class Row {
assertTextContent(actualShortAddress, expectedShortAddress); assertTextContent(actualShortAddress, expectedShortAddress);
} }
async expand (): Promise<void> { expand (): void {
const toggle = await within(this.primaryRow).findByTestId('row-toggle'); // the toggle is the row's expand column (Table.Column.Expand)
const toggle = this.primaryRow.querySelector('td.ui--Table-Column-Expand');
if (!toggle) {
throw new Error('Expected an expand toggle in the row');
}
fireEvent.click(toggle); fireEvent.click(toggle);
} }
@@ -1,9 +1,8 @@
// 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, within } from '@testing-library/react'; import { fireEvent, screen, within } from '@testing-library/react';
import assert from 'node:assert/strict';
import { assertTextContent } from '../utils/domAssertions.js'; import { assertTextContent } from '../utils/domAssertions.js';
import { JudgementTag } from './JudgementTag.js'; import { JudgementTag } from './JudgementTag.js';
@@ -42,7 +41,7 @@ export class Sidebar {
async assertAccountInput (expectedInput: string): Promise<void> { async assertAccountInput (expectedInput: string): Promise<void> {
const nameInput = await this.findByTestId('name-input'); const nameInput = await this.findByTestId('name-input');
expect(nameInput).toHaveProperty('value', expectedInput); assert.equal((nameInput as HTMLInputElement).value, expectedInput);
} }
async assertAccountName (expectedAccountName: string): Promise<void> { async assertAccountName (expectedAccountName: string): Promise<void> {
@@ -10,9 +10,8 @@ import { assertTextContent } from '../utils/domAssertions.js';
import { Row } from './Row.js'; import { Row } from './Row.js';
export class Table { export class Table {
constructor (private readonly table: HTMLElement, private readonly rowClassName: string) { constructor (private readonly table: HTMLElement) {
this.table = table; this.table = table;
this.rowClassName = rowClassName;
} }
async assertRowsOrder (balancesExpectedOrder: number[]): Promise<void> { async assertRowsOrder (balancesExpectedOrder: number[]): Promise<void> {
@@ -26,18 +25,28 @@ export class Table {
} }
} }
/**
* An item (account, contact) spans the rows from one marked isFirst up to
* the next: the main row, the total balance, and the expandable details
*/
async getRows (): Promise<Row[]> { async getRows (): Promise<Row[]> {
const htmlRows = await this.getFilteredHtmlRows(); const items: HTMLElement[][] = [];
const collapsibleRows: Row[] = [];
for (let rowIdx = 0; rowIdx < htmlRows.length; rowIdx = rowIdx + 2) { for (const htmlRow of await this.getBodyRows()) {
const primaryRow = htmlRows[rowIdx]; if (htmlRow.classList.contains('isFirst')) {
const detailsRow = htmlRows[rowIdx + 1]; items.push([htmlRow]);
} else if (items.length) {
collapsibleRows.push(new Row(primaryRow, detailsRow)); items[items.length - 1].push(htmlRow);
}
} }
return collapsibleRows; return items.map((rows) => {
if (rows.length !== 3) {
throw new Error(`Expected an item to span 3 rows, found ${rows.length}`);
}
return new Row(rows[0], rows[1], rows[2]);
});
} }
assertColumnNotExist (columnName: string): void { assertColumnNotExist (columnName: string): void {
@@ -52,15 +61,16 @@ export class Table {
return within(this.table).findByText(text); return within(this.table).findByText(text);
} }
private async getFilteredHtmlRows (): Promise<HTMLElement[]> { private async getBodyRows (): Promise<HTMLElement[]> {
const htmlRows = await this.getAllHtmlRows();
return htmlRows.filter((row) => row.className.startsWith(this.rowClassName));
}
private async getAllHtmlRows (): Promise<HTMLElement[]> {
const tableBody = this.table.getElementsByTagName('tbody')[0]; const tableBody = this.table.getElementsByTagName('tbody')[0];
return within(tableBody).findAllByRole('row'); if (!tableBody) {
return [];
}
// a collapsed details row is hidden, and still part of its item; rows of
// tables nested in a cell (e.g. a badge popup) are not items
return (await within(tableBody).findAllByRole('row', { hidden: true }))
.filter((row) => row.parentElement === tableBody);
} }
} }
@@ -24,6 +24,16 @@ export function assertHasClass (node: Node | null, className: string): void {
assert.ok(classList.contains(className), `Expected class "${className}" on ${describeElement(node)}`); assert.ok(classList.contains(className), `Expected class "${className}" on ${describeElement(node)}`);
} }
/** As not.toHaveClass: the element is there, without the class */
export function assertNotHasClass (node: Node | null, className: string): void {
assert.ok(node, `Expected an element without class "${className}", found none`);
const classList = (node as Partial<Element>).classList;
assert.ok(classList, `Expected an element without class "${className}", found ${describeElement(node)}`);
assert.ok(!classList.contains(className), `Expected no class "${className}" on ${describeElement(node)}`);
}
/** As toHaveTextContent: the whitespace-normalized text contains the string, or matches the pattern */ /** As toHaveTextContent: the whitespace-normalized text contains the string, or matches the pattern */
export function assertTextContent (element: Node | null, expected: string | RegExp): void { export function assertTextContent (element: Node | null, expected: string | RegExp): void {
assert.ok(element, `Expected an element with text ${String(expected)}, found none`); assert.ok(element, `Expected an element with text ${String(expected)}, found none`);