polkadot-js/apps

View on GitHub
packages/page-accounts/src/Accounts/index.spec.ts

Summary

Maintainability
F
6 days
Test Coverage
// Copyright 2017-2024 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0

/// <reference types="@polkadot/dev-test/globals.d.ts" />

import type { AddressFlags } from '@polkadot/react-hooks/types';
import type { Table } from '@polkadot/test-support/pagesElements';
import type { u32 } from '@polkadot/types';
import type { AccountId, Multisig, ProxyDefinition, Timepoint, Voting, VotingDelegating } from '@polkadot/types/interfaces';
import type { AccountRow } from '../../test/pageElements/AccountRow.js';

import { fireEvent, screen, within } from '@testing-library/react';

import { POLKADOT_GENESIS } from '@polkadot/apps-config';
import i18next from '@polkadot/react-components/i18n';
import { toShortAddress } from '@polkadot/react-components/util';
import { anAccountWithBalance, anAccountWithBalanceAndMeta, anAccountWithInfo, anAccountWithInfoAndMeta, anAccountWithMeta, anAccountWithStaking } from '@polkadot/test-support/creation/account';
import { makeStakingLedger as ledger } from '@polkadot/test-support/creation/staking';
import { alice, bob, MemoryStore } from '@polkadot/test-support/keyring';
import { balance, mockApiHooks, showBalance } from '@polkadot/test-support/utils';
import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';
import { BN } from '@polkadot/util';

import { AccountsPage } from '../../test/pages/accountsPage.js';

// FIXME isSplit Table
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Accounts page', () => {
  let accountsPage: AccountsPage;

  beforeAll(async () => {
    await i18next.changeLanguage('en');

    if (keyring.getAccounts().length === 0) {
      keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
    }
  });

  beforeEach(() => {
    accountsPage = new AccountsPage();
    accountsPage.clearAccounts();
  });

  describe('when no accounts', () => {
    beforeEach(() => {
      accountsPage.render([]);
    });

    // eslint-disable-next-line jest/expect-expect
    it('shows sort-by controls', async () => {
      await accountsPage.reverseSortingOrder();
    });

    it('shows a table', async () => {
      const accountsTable = await accountsPage.getTable();

      expect(accountsTable).not.toBeNull();
    });

    it('the accounts table contains no account rows', async () => {
      const accountRows = await accountsPage.getAccountRows();

      expect(accountRows).toHaveLength(0);
    });

    // eslint-disable-next-line jest/expect-expect
    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 accountsTable = await accountsPage.getTable();

      await accountsTable.assertText(noAccountsMessage);
    });

    it('no summary is displayed', () => {
      const summaries = screen.queryAllByTestId(/card-summary:total \w+/i);

      expect(summaries).toHaveLength(0);
    });
  });

  describe('when some accounts exist', () => {
    it('the accounts table contains some account rows', async () => {
      accountsPage.renderDefaultAccounts(2);
      const accountRows = await accountsPage.getAccountRows();

      expect(accountRows).toHaveLength(2);
    });

    // eslint-disable-next-line jest/expect-expect
    it('account rows display the total balance info', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ freeBalance: balance(500) }),
        anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
      );

      const rows = await accountsPage.getAccountRows();

      await rows[0].assertBalancesTotal(balance(500));
      await rows[1].assertBalancesTotal(balance(350));
    });

    // eslint-disable-next-line jest/expect-expect
    it('account rows display the details balance info', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ freeBalance: balance(500), lockedBalance: balance(30) }),
        anAccountWithBalance({ availableBalance: balance(50), freeBalance: balance(200), reservedBalance: balance(150) })
      );

      const rows = await accountsPage.getAccountRows();

      await rows[0].assertBalancesDetails([
        { amount: balance(0), name: 'transferrable' },
        { amount: balance(30), name: 'locked' }]);
      await rows[1].assertBalancesDetails([
        { amount: balance(50), name: 'transferrable' },
        { 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 () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithMeta({ isInjected: true, name: 'ALICE', whenCreated: 200 }),
        anAccountWithMeta({ name: 'ALICE_CHILD', parentAddress: alice, whenCreated: 300 })
      );

      const accountRows = await accountsPage.getAccountRows();

      expect(accountRows).toHaveLength(2);
      await accountRows[1].assertParentAccountName('ALICE');
    });

    // FIXME broken after column rework
    // eslint-disable-next-line jest/no-disabled-tests, jest/expect-expect
    it.skip('a separate column for parent account is not displayed', async () => {
      accountsPage.renderDefaultAccounts(1);
      const accountsTable = await accountsPage.getTable();

      accountsTable.assertColumnNotExist('parent');
      accountsTable.assertColumnExists('type');
    });

    it('account rows display the shorted address', async () => {
      accountsPage.renderAccountsForAddresses(
        alice
      );
      const accountRows = await accountsPage.getAccountRows();

      expect(accountRows).toHaveLength(1);
      const aliceShortAddress = toShortAddress(alice);

      await accountRows[0].assertShortAddress(aliceShortAddress);
    });

    // eslint-disable-next-line jest/expect-expect
    it('when account is not tagged, account row details displays none info', async () => {
      accountsPage.renderDefaultAccounts(1);
      const rows = await accountsPage.getAccountRows();

      await rows[0].assertTags('none');
    });

    // eslint-disable-next-line jest/expect-expect
    it('when account is tagged, account row details displays tags', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithInfo({ tags: ['my tag', 'Super Tag'] })
      );

      const rows = await accountsPage.getAccountRows();

      await rows[0].assertTags('my tagSuper Tag');
    });

    it('account details rows toggled on icon toggle click', async () => {
      accountsPage.renderDefaultAccounts(1);
      const row = (await accountsPage.getAccountRows())[0];

      expect(row.detailsRow).toHaveClass('isCollapsed');

      await row.expand();

      expect(row.detailsRow).toHaveClass('isExpanded');
    });

    it('displays some summary', () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ freeBalance: balance(500) }),
        anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
      );

      const summaries = screen.queryAllByTestId(/card-summary:total \w+/i);

      expect(summaries).not.toHaveLength(0);
    });

    it('displays balance summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ freeBalance: balance(500) }),
        anAccountWithBalance({ freeBalance: balance(200), reservedBalance: balance(150) })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?balance/i);

      expect(summary).toHaveTextContent(showBalance(500 + 200 + 150));
    });

    it('displays transferable summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ availableBalance: balance(400) }),
        anAccountWithBalance({ availableBalance: balance(600) })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?transferrable/i);

      expect(summary).toHaveTextContent(showBalance(400 + 600));
    });

    it('displays locked summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalance({ lockedBalance: balance(400) }),
        anAccountWithBalance({ lockedBalance: balance(600) })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?locked/i);

      expect(summary).toHaveTextContent(showBalance(400 + 600));
    });

    it('displays bonded summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithStaking({ stakingLedger: ledger(balance(70)) }),
        anAccountWithStaking({ stakingLedger: ledger(balance(20)) })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?bonded/i);

      expect(summary).toHaveTextContent(showBalance(70 + 20));
    });

    it('displays unbonding summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithStaking({
          unlocking: [
            {
              remainingEras: new BN('1000000000'),
              value: balance(200)
            },
            {
              remainingEras: new BN('2000000000'),
              value: balance(300)
            },
            {
              remainingEras: new BN('3000000000'),
              value: balance(400)
            }
          ]
        }),
        anAccountWithStaking({
          unlocking: [
            {
              remainingEras: new BN('1000000000'),
              value: balance(100)
            },
            {
              remainingEras: new BN('2000000000'),
              value: balance(200)
            },
            {
              remainingEras: new BN('3000000000'),
              value: balance(300)
            }
          ]
        })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?unbonding/i);

      expect(summary).toHaveTextContent(showBalance(200 + 300 + 400 + 100 + 200 + 300));
    });

    it('displays redeemable summary', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithStaking({ redeemable: balance(4000) }),
        anAccountWithStaking({ redeemable: balance(5000) })
      );

      const summary = await screen.findByTestId(/card-summary:(total )?redeemable/i);

      expect(summary).toHaveTextContent(showBalance(4000 + 5000));
    });

    it('sorts accounts by date by default', async () => {
      accountsPage.renderAccountsWithDefaultAddresses(
        anAccountWithBalanceAndMeta({ freeBalance: balance(1) }, { whenCreated: 200 }),
        anAccountWithBalanceAndMeta({ freeBalance: balance(2) }, { whenCreated: 300 }),
        anAccountWithBalanceAndMeta({ freeBalance: balance(3) }, { whenCreated: 100 })
      );
      expect(await accountsPage.getCurrentSortCategory()).toHaveTextContent('date');

      const accountsTable = await accountsPage.getTable();

      await accountsTable.assertRowsOrder([3, 1, 2]);
    });

    // FIXME multiple tables now
    // eslint-disable-next-line jest/no-disabled-tests
    describe.skip('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(3) }, { isInjected: true, name: 'aaa', whenCreated: 100 })
        );

        accountsTable = await accountsPage.getTable();
      });

      it('changes default dropdown value', async () => {
        await accountsPage.sortBy('balances');
        expect(await accountsPage.getCurrentSortCategory())
          .toHaveTextContent('balances');
      });

      // eslint-disable-next-line jest/expect-expect
      it('sorts by parent if asked', async () => {
        await accountsPage.sortBy('parent');
        await accountsTable.assertRowsOrder([3, 1, 2]);
      });

      // eslint-disable-next-line jest/expect-expect
      it('sorts by name if asked', async () => {
        await accountsPage.sortBy('name');
        await accountsTable.assertRowsOrder([3, 2, 1]);
      });

      // eslint-disable-next-line jest/expect-expect
      it('sorts by date if asked', async () => {
        await accountsPage.sortBy('date');
        await accountsTable.assertRowsOrder([3, 1, 2]);
      });

      // eslint-disable-next-line jest/expect-expect
      it('sorts by balances if asked', async () => {
        await accountsPage.sortBy('balances');
        await accountsTable.assertRowsOrder([1, 2, 3]);
      });

      // eslint-disable-next-line jest/expect-expect
      it('implements stable sort', async () => {
        await accountsPage.sortBy('name');
        await accountsTable.assertRowsOrder([3, 2, 1]);
        await accountsPage.sortBy('balances');
        await accountsTable.assertRowsOrder([1, 2, 3]);
      });

      // eslint-disable-next-line jest/expect-expect
      it('respects reverse button', async () => {
        await accountsPage.sortBy('name');
        await accountsTable.assertRowsOrder([3, 2, 1]);
        await accountsPage.sortBy('balances');
        await accountsTable.assertRowsOrder([1, 2, 3]);
        await accountsPage.reverseSortingOrder();
        await accountsTable.assertRowsOrder([3, 2, 1]);
        await accountsPage.sortBy('name');
        await accountsTable.assertRowsOrder([1, 2, 3]);
      });
    });
  });

  describe('badges', () => {
    let accountRows: AccountRow[];

    beforeEach(() => {
      mockApiHooks.setMultisigApprovals([
        [new TypeRegistry().createType('Hash', POLKADOT_GENESIS), {
          approvals: [bob as unknown as AccountId],
          deposit: balance(927000000000000),
          depositor: bob as unknown as AccountId,
          when: { height: new BN(1190) as u32, index: new BN(1) as u32 } as Timepoint
        } as Multisig
        ]
      ]);
      mockApiHooks.setDelegations([{ asDelegating: { target: bob as unknown as AccountId } as unknown as VotingDelegating, isDelegating: true } as Voting]);
      mockApiHooks.setProxies([[[{ delegate: alice as unknown as AccountId, proxyType: { isAny: true, isGovernance: true, isNonTransfer: true, isStaking: true, toNumber: () => 1 } } as unknown as ProxyDefinition], new BN(1)]]);
    });

    describe('when genesis hash is not set', () => {
      beforeEach(async () => {
        accountsPage.renderAccountsWithDefaultAddresses(
          anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { name: 'alice' }),
          anAccountWithMeta({ name: 'bob' })
        );
        accountRows = await accountsPage.getAccountRows();
      });

      describe('when isDevelopment flag', () => {
        let aliceRow: AccountRow;

        beforeEach(async () => {
          aliceRow = accountRows[0];
          await aliceRow.assertAccountName('ALICE');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the development badge is displayed', async () => {
          await aliceRow.assertBadge('wrench-badge');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the all networks badge is not displayed', () => {
          aliceRow.assertNoBadge('exclamation-triangle-badge');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the regular badge is not displayed', () => {
          aliceRow.assertNoBadge('transparent-badge');
        });
      });

      describe('when no isDevelopment flag', () => {
        let bobRow: AccountRow;

        beforeEach(async () => {
          bobRow = accountRows[1];
          await bobRow.assertAccountName('BOB');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the development badge is not displayed', () => {
          bobRow.assertNoBadge('wrench-badge');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the all networks badge is displayed', async () => {
          await bobRow.assertBadge('exclamation-triangle-badge');
        });

        // eslint-disable-next-line jest/expect-expect
        it('the regular badge is not displayed', () => {
          bobRow.assertNoBadge('transparent-badge');
        });
      });
    });

    describe('when genesis hash set', () => {
      beforeEach(async () => {
        accountsPage.renderAccountsWithDefaultAddresses(
          anAccountWithInfoAndMeta({ flags: { isDevelopment: true } as AddressFlags }, { genesisHash: '0x1234', name: 'charlie' })
        );
        accountRows = await accountsPage.getAccountRows();
      });

      // eslint-disable-next-line jest/expect-expect
      it('the development badge is not displayed', () => {
        accountRows[0].assertNoBadge('wrench-badge');
      });

      // eslint-disable-next-line jest/expect-expect
      it('the all networks badge is not displayed', () => {
        accountRows[0].assertNoBadge('exclamation-triangle-badge');
      });

      // eslint-disable-next-line jest/expect-expect
      it('the regular badge is displayed', async () => {
        await accountRows[0].assertBadge('badge');
      });
    });

    describe('show popups', () => {
      beforeEach(async () => {
        accountsPage.renderAccountsWithDefaultAddresses(
          anAccountWithInfoAndMeta({ flags: { isDevelopment: 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.*/);

        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 approvalsModalToggle = await within(badgePopup).findByText('View pending approvals');

        fireEvent.click(approvalsModalToggle);
        const modal = await screen.findByTestId('modal');

        within(modal).getByText('Pending call hashes');
        expect(approvalsModalToggle).toHaveClass('purpleColor');
      });

      it('delegate democracy vote', async () => {
        await accountRows[0].assertBadge('calendar-check-badge');
        const badgePopup = getPopupById(/calendar-check-badge-hover.*/);
        const delegateModalToggle = await within(badgePopup).findByText('Manage delegation');

        fireEvent.click(delegateModalToggle);
        const modal = await screen.findByTestId('modal');

        within(modal).getByText('democracy vote delegation');
        expect(delegateModalToggle).toHaveClass('normalColor');
      });

      it('proxy overview', async () => {
        await accountRows[0].assertBadge('sitemap-badge');
        const badgePopup = getPopupById(/sitemap-badge-hover.*/);
        const proxyOverviewToggle = await within(badgePopup).findByText('Manage proxies');

        fireEvent.click(proxyOverviewToggle);
        const modal = await screen.findByTestId('modal');

        within(modal).getByText('Proxy overview');
        expect(proxyOverviewToggle).toHaveClass('normalColor');
      });

      afterEach(() => {
        mockApiHooks.setMultisigApprovals([]);
      });
    });

    function getPopupById (popupId: RegExp): HTMLElement {
      const badgePopup = accountsPage.getById(popupId);

      if (!badgePopup) {
        throw new Error('badge popup should be found');
      }

      return badgePopup;
    }
  });
});