From 3767c819e36fde9d3ec7d5e051670e3e45217886 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 10 Oct 2026 14:12:42 +0400 Subject: [PATCH] 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. --- .../page-accounts/src/Accounts/index.spec.ts | 95 +++++++++---------- .../page-accounts/test/pages/accountsPage.tsx | 15 ++- .../page-addresses/src/Contacts/index.spec.ts | 6 +- .../test/pages/addressesPage.tsx | 3 +- .../src/AccountSidebar/Sidebar.spec.tsx | 20 ++-- packages/test-support/src/mockData/index.ts | 4 - packages/test-support/src/pages/Page.tsx | 24 +++-- .../test-support/src/pagesElements/Row.ts | 15 ++- .../test-support/src/pagesElements/Sidebar.ts | 5 +- .../test-support/src/pagesElements/Table.ts | 46 +++++---- .../test-support/src/utils/domAssertions.ts | 10 ++ 11 files changed, 136 insertions(+), 107 deletions(-) diff --git a/packages/page-accounts/src/Accounts/index.spec.ts b/packages/page-accounts/src/Accounts/index.spec.ts index 2438b48..a74123d 100644 --- a/packages/page-accounts/src/Accounts/index.spec.ts +++ b/packages/page-accounts/src/Accounts/index.spec.ts @@ -9,15 +9,14 @@ import type { u32 } from '@pezkuwi/types'; import type { AccountId, Multisig, ProxyDefinition, Timepoint, Voting, VotingDelegating } from '@pezkuwi/types/interfaces'; 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 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 { makeStakingLedger as ledger } from '@pezkuwi/test-support/creation/staking'; 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 { keyring } from '@pezkuwi/ui-keyring'; import { BN } from '@pezkuwi/util'; @@ -28,9 +27,10 @@ await mockAccountsPageHooks(); const { AccountsPage } = await import('../../test/pages/accountsPage.js'); -// FIXME isSplit Table -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('Accounts page', () => { +// the totals always shown; bonded, redeemable and unbonding only when non-zero +const SUMMARY_TOTALS = ['card-summary:total balance', 'card-summary:total transferable', 'card-summary:total locked']; + +describe('Accounts page', () => { let accountsPage: InstanceType; beforeAll(async () => { @@ -75,10 +75,11 @@ describe.skip('Accounts page', () => { await accountsTable.assertText(noAccountsMessage); }); - it('no summary is displayed', () => { - const summaries = screen.queryAllByTestId(/card-summary:total \w+/i); + it('the summary shows placeholders, and no staking totals', async () => { + 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' }]); }); - // FIXME multiple tables - // eslint-disable-next-line jest/no-disabled-tests - it.skip('derived account displays parent account info', async () => { + it('derived account displays parent account info', async () => { accountsPage.renderAccountsWithDefaultAddresses( anAccountWithMeta({ isInjected: true, name: 'ALICE', whenCreated: 200 }), anAccountWithMeta({ name: 'ALICE_CHILD', parentAddress: alice, whenCreated: 300 }) @@ -134,26 +133,25 @@ describe.skip('Accounts page', () => { await accountRows[1].assertParentAccountName('ALICE'); }); - // FIXME broken after column rework - // eslint-disable-next-line jest/no-disabled-tests - it.skip('a separate column for parent account is not displayed', async () => { + it('parent and type are not separate columns, the type is in the row details', async () => { accountsPage.renderDefaultAccounts(1); const accountsTable = await accountsPage.getTable(); + const rows = await accountsTable.getRows(); 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( alice ); const accountRows = await accountsPage.getAccountRows(); expect(accountRows).toHaveLength(1); - const aliceShortAddress = toShortAddress(alice); - - await accountRows[0].assertShortAddress(aliceShortAddress); + // shown in full, shortened by css + await accountRows[0].assertShortAddress(alice); }); // eslint-disable-next-line jest/expect-expect @@ -181,20 +179,23 @@ describe.skip('Accounts page', () => { assertHasClass(row.detailsRow, 'isCollapsed'); - await row.expand(); + row.expand(); assertHasClass(row.detailsRow, 'isExpanded'); }); - it('displays some summary', () => { + it('displays the summary totals, not placeholders', async () => { accountsPage.renderAccountsWithDefaultAddresses( anAccountWithBalance({ freeBalance: balance(500) }), 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 () => { @@ -306,21 +307,14 @@ describe.skip('Accounts page', () => { await accountsTable.assertRowsOrder([3, 1, 2]); }); - // FIXME multiple tables now - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('when sorting is used', () => { + // the page sorts within each account group, so all three share a group + describe('when sorting is used', () => { let accountsTable: Table; beforeEach(async () => { accountsPage.renderAccountsWithDefaultAddresses( anAccountWithBalanceAndMeta({ freeBalance: balance(1) }, { isInjected: true, name: 'bbb', whenCreated: 200 }), - anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { - hardwareType: 'ledger', - isHardware: true, - name: 'bb', - parentAddress: alice, - whenCreated: 300 - }), + anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { isInjected: true, name: 'bb', parentAddress: alice, whenCreated: 300 }), anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { isInjected: true, name: 'aaa', whenCreated: 100 }) ); @@ -468,22 +462,20 @@ describe.skip('Accounts page', () => { describe('show popups', () => { beforeEach(async () => { 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(); }); // eslint-disable-next-line jest/expect-expect it('development', async () => { - await accountRows[0].assertBadge('wrench-badge'); - const badgePopup = getPopupById(/wrench-badge-hover.*/); + const badgePopup = await openBadgePopup('wrench-badge'); 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 () => { - await accountRows[0].assertBadge('file-signature-badge'); - const badgePopup = getPopupById(/file-signature-badge-hover.*/); + const badgePopup = await openBadgePopup('file-signature-badge'); const approvalsModalToggle = await within(badgePopup).findByText('View pending approvals'); fireEvent.click(approvalsModalToggle); @@ -494,8 +486,7 @@ describe.skip('Accounts page', () => { }); it('delegate democracy vote', async () => { - await accountRows[0].assertBadge('calendar-check-badge'); - const badgePopup = getPopupById(/calendar-check-badge-hover.*/); + const badgePopup = await openBadgePopup('calendar-check-badge'); const delegateModalToggle = await within(badgePopup).findByText('Manage delegation'); fireEvent.click(delegateModalToggle); @@ -506,8 +497,7 @@ describe.skip('Accounts page', () => { }); it('proxy overview', async () => { - await accountRows[0].assertBadge('sitemap-badge'); - const badgePopup = getPopupById(/sitemap-badge-hover.*/); + const badgePopup = await openBadgePopup('sitemap-badge'); const proxyOverviewToggle = await within(badgePopup).findByText('Manage proxies'); fireEvent.click(proxyOverviewToggle); @@ -522,14 +512,21 @@ describe.skip('Accounts page', () => { }); }); - function getPopupById (popupId: RegExp): HTMLElement { - const badgePopup = accountsPage.getById(popupId); + // a badge renders its popup while hovered + async function openBadgePopup (badgeTestId: string): Promise { + const badge = await within(accountRows[0].primaryRow).findByTestId(badgeTestId); - if (!badgePopup) { - throw new Error('badge popup should be found'); - } + fireEvent.mouseEnter(badge); - return badgePopup; + return waitFor(() => { + const badgePopup = accountsPage.getById(new RegExp(`^${badgeTestId}-hover-`)); + + if (!badgePopup) { + throw new Error(`${badgeTestId} popup should be found`); + } + + return badgePopup; + }); } }); }); diff --git a/packages/page-accounts/test/pages/accountsPage.tsx b/packages/page-accounts/test/pages/accountsPage.tsx index f696571..d61c1d8 100644 --- a/packages/page-accounts/test/pages/accountsPage.tsx +++ b/packages/page-accounts/test/pages/accountsPage.tsx @@ -23,16 +23,21 @@ settings.set({ ...settings.get(), storage: 'on' }); export class AccountsPage extends Page { constructor () { super( - , - 'Account-' + ); } + /** The rows of every account group table, in the order shown */ async getAccountRows (): Promise { - const table = await this.getTable(); - const rows = await table.getRows(); + const rows: AccountRow[] = []; - 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 { diff --git a/packages/page-addresses/src/Contacts/index.spec.ts b/packages/page-addresses/src/Contacts/index.spec.ts index 935f55e..064f2c4 100644 --- a/packages/page-addresses/src/Contacts/index.spec.ts +++ b/packages/page-addresses/src/Contacts/index.spec.ts @@ -19,9 +19,7 @@ await mockAddressesPageHooks(); const { AddressesPage } = await import('../../test/pages/addressesPage.js'); -// FIXME isSplit Table -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('Addresses page', () => { +describe('Addresses page', () => { let addressesPage: InstanceType; beforeAll(async () => { @@ -133,7 +131,7 @@ describe.skip('Addresses page', () => { assertHasClass(row.detailsRow, 'isCollapsed'); - await row.expand(); + row.expand(); assertHasClass(row.detailsRow, 'isExpanded'); }); diff --git a/packages/page-addresses/test/pages/addressesPage.tsx b/packages/page-addresses/test/pages/addressesPage.tsx index fb10906..c776bbd 100644 --- a/packages/page-addresses/test/pages/addressesPage.tsx +++ b/packages/page-addresses/test/pages/addressesPage.tsx @@ -16,8 +16,7 @@ const NOOP_CHANGE = () => undefined; export class AddressesPage extends Page { constructor () { super( - , - 'Address-' + ); } diff --git a/packages/react-components/src/AccountSidebar/Sidebar.spec.tsx b/packages/react-components/src/AccountSidebar/Sidebar.spec.tsx index 5fcdb50..d59ecaa 100644 --- a/packages/react-components/src/AccountSidebar/Sidebar.spec.tsx +++ b/packages/react-components/src/AccountSidebar/Sidebar.spec.tsx @@ -11,7 +11,7 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react'; import i18next from '@pezkuwi/react-components/i18n'; 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 { keyring } from '@pezkuwi/ui-keyring'; @@ -19,12 +19,10 @@ import { mockAccountsPageHooks } from '../../../page-accounts/test/mockAccountsP 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'); -// FIXME: these all need to be wrapped in waitFor .... -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('Sidebar', () => { +describe('Sidebar', () => { let accountsPage: InstanceType; let sideBar: Sidebar; @@ -45,7 +43,6 @@ describe.skip('Sidebar', () => { const initialName = 'INITIAL_NAME'; const newName = 'NEW_NAME'; const defaultTag = 'Default'; - const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]'; describe('changes name', () => { beforeEach(async () => { @@ -83,8 +80,7 @@ describe.skip('Sidebar', () => { sideBar = await accountsPage.openSidebarForRow(0); await sideBar.clickByText('none'); expect(sideBar.queryByRole('combobox')).toBeFalsy(); - - await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError); + expect(sideBar.queryByTestId('name-input')).toBeFalsy(); }); it('when isEditable is false account name is not editable', async () => { @@ -94,7 +90,7 @@ describe.skip('Sidebar', () => { sideBar = await accountsPage.openSidebarForRow(0); sideBar.edit(); - await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError); + expect(sideBar.queryByTestId('name-input')).toBeFalsy(); }); describe('on edit cancel', () => { @@ -137,7 +133,7 @@ describe.skip('Sidebar', () => { await sideBar.typeAccountName(newName); await sideBar.selectTag(defaultTag); - fireEvent.click(await screen.findByText('accounts')); + fireEvent.click(await screen.findByTestId('sort-by-section')); await sideBar.assertTags('none'); await sideBar.assertAccountName('ALICE'); @@ -236,13 +232,13 @@ describe.skip('Sidebar', () => { it('singular registrar', async () => { const judgementTag = await sideBar.getJudgement('1 Known good'); - await judgementTag.assertRegistrars([charlieShortAddress]); + await judgementTag.assertRegistrars([charlie]); }); it('multiple registrars', async () => { 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 () => { diff --git a/packages/test-support/src/mockData/index.ts b/packages/test-support/src/mockData/index.ts index 9203746..16a8bd0 100644 --- a/packages/test-support/src/mockData/index.ts +++ b/packages/test-support/src/mockData/index.ts @@ -41,7 +41,3 @@ export const charlieRegistrar: Registrar = { address: charlie, index: 1 }; export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 }; export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar]; - -export const bobShortAddress = '5FHneW…M694ty'; -export const charlieShortAddress = '5DAAnr…3PTXFy'; -export const ferdieShortAddress = '5CiPPs…SK2DjL'; diff --git a/packages/test-support/src/pages/Page.tsx b/packages/test-support/src/pages/Page.tsx index 92da09b..8bb6301 100644 --- a/packages/test-support/src/pages/Page.tsx +++ b/packages/test-support/src/pages/Page.tsx @@ -16,6 +16,7 @@ import { ThemeProvider } from 'styled-components'; import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config'; import { AccountSidebar, lightTheme } from '@pezkuwi/react-components'; 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 { TypeRegistry } from '@pezkuwi/types/create'; import { keyring } from '@pezkuwi/ui-keyring'; @@ -34,9 +35,8 @@ export abstract class Page { private renderResult?: RenderResult; 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.rowClassName = rowClassName; } render (accounts: [string, AccountOverrides][]): void { @@ -59,6 +59,9 @@ export abstract class Page { proxy: { proxyDepositBase: new BN(1), proxyDepositFactor: new BN(1) + }, + system: { + ss58Prefix: new BN(42) } }, createType: () => ({ @@ -141,9 +144,11 @@ export abstract class Page { - - {React.cloneElement(this.overview, { onStatusChange: noop }) } - + + + {React.cloneElement(this.overview, { onStatusChange: noop }) } + + @@ -156,7 +161,14 @@ export abstract class Page { async getTable (): Promise { 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 { + this.assertRendered(); + + return (await screen.findAllByRole('table')).map((table) => new Table(table)); } clearAccounts (): void { diff --git a/packages/test-support/src/pagesElements/Row.ts b/packages/test-support/src/pagesElements/Row.ts index 033cb69..bc0a5d7 100644 --- a/packages/test-support/src/pagesElements/Row.ts +++ b/packages/test-support/src/pagesElements/Row.ts @@ -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 export class Row { public primaryRow: HTMLElement; + public balanceRow: HTMLElement; public detailsRow: HTMLElement; - constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) { + constructor (primaryRow: HTMLElement, balanceRow: HTMLElement, detailsRow: HTMLElement) { this.primaryRow = primaryRow; + this.balanceRow = balanceRow; this.detailsRow = detailsRow; } @@ -29,7 +31,7 @@ export class Row { } async getBalanceSummary (): Promise { - return within(this.primaryRow).findByTestId('balance-summary'); + return within(this.balanceRow).findByTestId('balance-summary'); } async assertAccountName (expectedName: string): Promise { @@ -64,8 +66,13 @@ export class Row { assertTextContent(actualShortAddress, expectedShortAddress); } - async expand (): Promise { - const toggle = await within(this.primaryRow).findByTestId('row-toggle'); + expand (): void { + // 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); } diff --git a/packages/test-support/src/pagesElements/Sidebar.ts b/packages/test-support/src/pagesElements/Sidebar.ts index c75208a..72f7e18 100644 --- a/packages/test-support/src/pagesElements/Sidebar.ts +++ b/packages/test-support/src/pagesElements/Sidebar.ts @@ -1,9 +1,8 @@ // Copyright 2017-2026 @pezkuwi/test-supports authors & contributors // SPDX-License-Identifier: Apache-2.0 -/* global expect */ - import { fireEvent, screen, within } from '@testing-library/react'; +import assert from 'node:assert/strict'; import { assertTextContent } from '../utils/domAssertions.js'; import { JudgementTag } from './JudgementTag.js'; @@ -42,7 +41,7 @@ export class Sidebar { async assertAccountInput (expectedInput: string): Promise { const nameInput = await this.findByTestId('name-input'); - expect(nameInput).toHaveProperty('value', expectedInput); + assert.equal((nameInput as HTMLInputElement).value, expectedInput); } async assertAccountName (expectedAccountName: string): Promise { diff --git a/packages/test-support/src/pagesElements/Table.ts b/packages/test-support/src/pagesElements/Table.ts index 45b0b90..8950c0d 100644 --- a/packages/test-support/src/pagesElements/Table.ts +++ b/packages/test-support/src/pagesElements/Table.ts @@ -10,9 +10,8 @@ import { assertTextContent } from '../utils/domAssertions.js'; import { Row } from './Row.js'; export class Table { - constructor (private readonly table: HTMLElement, private readonly rowClassName: string) { + constructor (private readonly table: HTMLElement) { this.table = table; - this.rowClassName = rowClassName; } async assertRowsOrder (balancesExpectedOrder: number[]): Promise { @@ -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 { - const htmlRows = await this.getFilteredHtmlRows(); - const collapsibleRows: Row[] = []; + const items: HTMLElement[][] = []; - for (let rowIdx = 0; rowIdx < htmlRows.length; rowIdx = rowIdx + 2) { - const primaryRow = htmlRows[rowIdx]; - const detailsRow = htmlRows[rowIdx + 1]; - - collapsibleRows.push(new Row(primaryRow, detailsRow)); + for (const htmlRow of await this.getBodyRows()) { + if (htmlRow.classList.contains('isFirst')) { + items.push([htmlRow]); + } else if (items.length) { + 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 { @@ -52,15 +61,16 @@ export class Table { return within(this.table).findByText(text); } - private async getFilteredHtmlRows (): Promise { - const htmlRows = await this.getAllHtmlRows(); - - return htmlRows.filter((row) => row.className.startsWith(this.rowClassName)); - } - - private async getAllHtmlRows (): Promise { + private async getBodyRows (): Promise { 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); } } diff --git a/packages/test-support/src/utils/domAssertions.ts b/packages/test-support/src/utils/domAssertions.ts index d34ef48..b493b3d 100644 --- a/packages/test-support/src/utils/domAssertions.ts +++ b/packages/test-support/src/utils/domAssertions.ts @@ -24,6 +24,16 @@ export function assertHasClass (node: Node | null, className: string): void { 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).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 */ export function assertTextContent (element: Node | null, expected: string | RegExp): void { assert.ok(element, `Expected an element with text ${String(expected)}, found none`);