Skip to content
223 changes: 223 additions & 0 deletions bridge/reporting/__tests__/components/ReportTable.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import ReportTable from '@/components/ReportTable';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, within } from '@testing-library/react';

const REQUEST_TAB = {
resource: 'requests',
sourceAsset: { ticker: 'XYM', divisibility: 6 },
destinationAsset: { ticker: 'WXYM', divisibility: 6 },
sourceNetwork: 'nativeNetwork',
destinationNetwork: 'wrappedNetwork'
};

const ERROR_TAB = {
...REQUEST_TAB,
resource: 'errors'
};

const CONFIGURATION = {
nativeNetwork: {
blockchain: 'symbol',
explorerUrl: 'https://symbol.example'
},
wrappedNetwork: {
blockchain: 'ethereum',
explorerUrl: 'https://ethereum.example'
}
};

const REQUEST_ROW = {
destinationAddress: '0x1f533cd9711049fA7604D0F49C45B6e5Af30ef8e',
errorMessage: null,
payoutConversionRate: '1000000',
payoutNetAmount: '299642570825',
payoutSentTimestamp: 4,
payoutStatus: 2,
payoutTimestamp: 3,
payoutTotalFee: '357429175',
payoutTransactionHash: 'A'.repeat(64),
payoutTransactionHeight: '11',
requestAmount: '300000000000',
requestTimestamp: 2,
requestTransactionHash: 'B'.repeat(64),
requestTransactionHeight: '10',
requestTransactionSubindex: -1,
senderAddress: 'TCONKG47FW2ZEZBPV6G7F422LXBDSMVT3JMYM4I'
};

const ERROR_ROW = {
errorMessage: 'Required message is missing',
requestTimestamp: 5,
requestTransactionHash: 'C'.repeat(64),
requestTransactionHeight: '13',
requestTransactionSubindex: -1,
senderAddress: 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY'
};

const renderTable = ({
configuration = CONFIGURATION,
onSortChange = jest.fn(),
rows = [REQUEST_ROW],
sort = 0,
tab = REQUEST_TAB
} = {}) => {
const reportTable = (
<ReportTable
configuration={configuration}
onSortChange={onSortChange}
rows={rows}
sort={sort}
tab={tab}
/>
);
const result = render(reportTable);

return {
...result,
onSortChange,
table: screen.getByRole('table'),
mobileCard: screen.getByRole('article')
};
};

describe('ReportTable', () => {
it('renders and formats request report fields', () => {
// Arrange:
const { table } = renderTable();

// Act:
const tableView = within(table);

// Assert:
expect(tableView.getByText('Completed')).toBeInTheDocument();
expect(tableView.getByText('TCONKG47F…JMYM4I')).toHaveAttribute('title', 'TCONKG47FW2ZEZBPV6G7F422LXBDSMVT3JMYM4I');
expect(tableView.getByText('AAAAAAAA…AAAAAA')).toHaveAttribute('title', 'A'.repeat(64));
expect(tableView.getByText('300000')).toBeInTheDocument();
expect(tableView.getByText('0x1f533cd…30ef8e')).toHaveAttribute('title', '0x1f533cd9711049fA7604D0F49C45B6e5Af30ef8e');
expect(tableView.getByText('BBBBBBBB…BBBBBB')).toHaveAttribute('title', 'B'.repeat(64));
expect(tableView.getByText('1')).toHaveAttribute('title', '1000000 PPM');
expect(tableView.getByText('357.429175')).toBeInTheDocument();
expect(tableView.getByText('299642.570825')).toBeInTheDocument();
expect(tableView.getByText('1970-01-01 00:00:02 UTC')).toBeInTheDocument();
expect(tableView.getByText('1970-01-01 00:00:03 UTC')).toBeInTheDocument();
});

it('renders the active sort direction and handles sort changes', () => {
// Arrange:
const onSortChange = jest.fn();
const { table } = renderTable({ onSortChange });
const sortButton = within(table).getByRole('button', {
name: 'Sort by request block height ascending'
});

// Act:
fireEvent.click(sortButton);

// Assert:
expect(onSortChange).toHaveBeenCalledTimes(1);
expect(sortButton.closest('th')).toHaveAttribute('aria-sort', 'descending');
});

const runDescendingSortTest = params => {
// Arrange:
const { table } = renderTable(params);

// Act:
const sortButton = within(table).getByRole('button', {
name: 'Sort by request block height descending'
});

// Assert:
expect(sortButton).toHaveTextContent('↑');
expect(sortButton.closest('th')).toHaveAttribute('aria-sort', 'ascending');
};

it('renders the descending sort direction for request reports', () => {
runDescendingSortTest({ sort: 1 });
});

it('renders the descending sort direction for error reports', () => {
runDescendingSortTest({
rows: [ERROR_ROW],
sort: 1,
tab: ERROR_TAB
});
});

it('renders and formats errors report fields', () => {
// Arrange:
const { table } = renderTable({
rows: [ERROR_ROW],
tab: ERROR_TAB
});

// Act:
const tableView = within(table);

// Assert:
expect(tableView.getByText('TARDV42KT…IXVJQY')).toHaveAttribute('title', 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY');
expect(tableView.getByText('CCCCCCCC…CCCCCC')).toHaveAttribute('title', 'C'.repeat(64));
expect(tableView.getByText('Required message is missing')).toBeInTheDocument();
});

it('renders and formats request mobile card fields', () => {
// Arrange:
const { mobileCard } = renderTable();

// Act:
const cardView = within(mobileCard);

// Assert:
expect(cardView.getByText('#10')).toBeInTheDocument();
expect(cardView.getByText('Completed')).toBeInTheDocument();
expect(cardView.getByText('Sender')).toBeInTheDocument();
expect(cardView.getByText('TCONKG47F…JMYM4I')).toHaveAttribute('title', REQUEST_ROW.senderAddress);
expect(cardView.getByText('Request')).toBeInTheDocument();
expect(cardView.getByText('BBBBBBBB…BBBBBB')).toHaveAttribute('title', REQUEST_ROW.requestTransactionHash);
expect(cardView.getByText('300000')).toBeInTheDocument();
expect(cardView.getByText('Payout Address')).toBeInTheDocument();
expect(cardView.getByText('0x1f533cd…30ef8e')).toHaveAttribute('title', REQUEST_ROW.destinationAddress);
expect(cardView.getByText('AAAAAAAA…AAAAAA')).toHaveAttribute('title', REQUEST_ROW.payoutTransactionHash);
expect(cardView.getByText('1')).toHaveAttribute('title', '1000000 PPM');
expect(cardView.getByText('357.429175')).toBeInTheDocument();
expect(cardView.getByText('299642.570825')).toBeInTheDocument();
});

it('renders and formats errors mobile card fields', () => {
// Arrange:
const { mobileCard } = renderTable({
rows: [ERROR_ROW],
tab: ERROR_TAB
});

// Act:
const cardView = within(mobileCard);

// Assert:
expect(cardView.getByText('#13')).toBeInTheDocument();
expect(cardView.getByText('Error')).toBeInTheDocument();
expect(cardView.getByText('Sender')).toBeInTheDocument();
expect(cardView.getByText('TARDV42KT…IXVJQY')).toHaveAttribute('title', ERROR_ROW.senderAddress);
expect(cardView.getByText('Request')).toBeInTheDocument();
expect(cardView.getByText('CCCCCCCC…CCCCCC')).toHaveAttribute('title', ERROR_ROW.requestTransactionHash);
expect(cardView.getByText('1970-01-01 00:00:05 UTC')).toBeInTheDocument();
expect(cardView.getByText('Required message is missing')).toBeInTheDocument();
});

it('renders a placeholder when an error message is unavailable', () => {
// Arrange:
const row = {
...ERROR_ROW,
errorMessage: null
};

// Act:
renderTable({
rows: [row],
tab: ERROR_TAB
});

// Assert:
expect(screen.getAllByText('—')).toHaveLength(2);
});
});
143 changes: 143 additions & 0 deletions bridge/reporting/__tests__/components/ReportTableFields.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
AmountValue,
ExternalValue,
RateValue,
StatusBadge,
TransactionValue
} from '@/components/ReportTableFields';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';

const SYMBOL_NETWORK = {
blockchain: 'symbol',
explorerUrl: 'https://symbol.example'
};

describe('StatusBadge', () => {
it('renders the configured payout status', () => {
// Act:
render(<StatusBadge errorMessage={null} status={2} />);

// Assert:
expect(screen.getByText('Completed')).toBeInTheDocument();
});

it('renders error details through an accessible tooltip', () => {
// Act:
render(<StatusBadge errorMessage="Payout rejected" status={3} />);
const failedStatus = screen.getByLabelText('Failed: Payout rejected');

// Assert:
expect(failedStatus).toHaveTextContent('Failed');
expect(failedStatus).toHaveAttribute('data-tooltip', 'Payout rejected');
expect(failedStatus).toHaveAttribute('tabindex', '0');
});

it('renders an unknown label for an unconfigured payout status', () => {
// Act:
render(<StatusBadge errorMessage={null} status={99} />);

// Assert:
expect(screen.getByText('Unknown')).toBeInTheDocument();
});
});

describe('ExternalValue', () => {
it('renders a truncated value linked to the configured explorer', () => {
// Arrange:
const value = 'TCONKG47FW2ZEZBPV6G7F422LXBDSMVT3JMYM4I';

// Act:
render(<ExternalValue network={SYMBOL_NETWORK} type="address" value={value} />);
const link = screen.getByRole('link');

// Assert:
expect(link).toHaveTextContent('TCONKG47F…JMYM4I');
expect(link).toHaveAttribute('href', `https://symbol.example/accounts/${value}`);
expect(link).toHaveAttribute('title', value);
expect(link).toHaveAttribute('rel', 'noreferrer');
expect(link).toHaveAttribute('target', '_blank');
});

it('renders a value without a link when an explorer URL is unavailable', () => {
// Arrange:
const value = 'TCONKG47FW2ZEZBPV6G7F422LXBDSMVT3JMYM4I';

// Act:
render(<ExternalValue network={{ blockchain: 'symbol' }} type="address" value={value} />);
const externalValue = screen.getByTitle(value);

// Assert:
expect(externalValue).toHaveTextContent('TCONKG47F…JMYM4I');
expect(externalValue).not.toHaveAttribute('href');
});

it('renders a placeholder and empty title when the value is unavailable', () => {
// Act:
render(<ExternalValue network={SYMBOL_NETWORK} type="address" value={null} />);
const placeholder = screen.getByText('—');

// Assert:
expect(placeholder).toHaveAttribute('title', '');
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
});

describe('TransactionValue', () => {
it('renders the formatted timestamp and linked transaction hash', () => {
// Arrange:
const hash = 'A'.repeat(64);

// Act:
render(<TransactionValue hash={hash} network={SYMBOL_NETWORK} timestamp={2} />);
const link = screen.getByRole('link');

// Assert:
expect(screen.getByText('1970-01-01 00:00:02 UTC')).toBeInTheDocument();
expect(link).toHaveTextContent('AAAAAAAA…AAAAAA');
expect(link).toHaveAttribute('href', `https://symbol.example/transactions/${hash}`);
expect(link).toHaveAttribute('title', hash);
});
});

describe('AmountValue', () => {
const asset = { ticker: 'XYM', divisibility: 6 };

it('renders a formatted amount with its asset ticker and raw value', () => {
// Act:
render(<AmountValue asset={asset} value="300000000000" />);
const amount = screen.getByTitle('300000000000');

// Assert:
expect(amount).toHaveTextContent('300000');
expect(amount).toHaveTextContent('XYM');
});

it.each([null, undefined])('renders a placeholder and empty title when the value is %s', value => {
// Act:
render(<AmountValue asset={asset} value={value} />);
const placeholder = screen.getByText('—');

// Assert:
expect(placeholder.parentElement).toHaveAttribute('title', '');
expect(screen.queryByText('XYM')).not.toBeInTheDocument();
});
});

describe('RateValue', () => {
it('renders a formatted rate with its raw PPM value', () => {
// Act:
render(<RateValue value="1000000" />);

// Assert:
expect(screen.getByText('1')).toHaveAttribute('title', '1000000 PPM');
});

it.each([null, undefined])('renders a placeholder and empty title when the value is %s', value => {
// Act:
render(<RateValue value={value} />);

// Assert:
expect(screen.getByText('—')).toHaveAttribute('title', '');
});
});
26 changes: 26 additions & 0 deletions bridge/reporting/__tests__/components/SortHeader.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SortHeader from '@/components/SortHeader';
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';

describe('SortHeader', () => {
it.each([
[0, 'ascending', '↓'],
[1, 'descending', '↑']
])('renders sort value %s as %s and handles a sort change', (sort, direction, indicator) => {
// Arrange:
const onSortChange = jest.fn();
render(<SortHeader onSortChange={onSortChange} sort={sort} />);
const button = screen.getByRole('button', {
name: `Sort by request block height ${direction}`
});

// Act:
fireEvent.click(button);

// Assert:
expect(button).toHaveTextContent('Request hash');
expect(button).toHaveTextContent(indicator);
expect(button).toHaveAttribute('title', 'Sorted by request block height');
expect(onSortChange).toHaveBeenCalledTimes(1);
});
});
Loading