@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
375 lines (330 loc) • 11.3 kB
text/typescript
/**
* @jest-environment jsdom
*/
import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors";
import { renderHook } from "@testing-library/react";
import type { AddressSearchResult } from "../../types";
import { useRecipientSearchState } from "../useRecipientSearchState";
const createDefaultResult = (overrides?: Partial<AddressSearchResult>): AddressSearchResult =>
({
status: "idle",
error: null,
resolvedAddress: undefined,
ensName: undefined,
isLedgerAccount: false,
accountName: undefined,
accountBalance: undefined,
accountBalanceFormatted: undefined,
isFirstInteraction: true,
matchedRecentAddress: undefined,
matchedAccounts: [],
bridgeErrors: undefined,
bridgeWarnings: undefined,
...overrides,
}) as AddressSearchResult;
const defaultProps = {
searchValue: "",
result: createDefaultResult(),
isLoading: false,
recipientSupportsDomain: false,
};
describe("useRecipientSearchState", () => {
it("should hide search results when search value is empty", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "",
}),
);
expect(result.current.showSearchResults).toBe(false);
expect(result.current.showMatchedAddress).toBe(false);
expect(result.current.showAddressValidationError).toBe(false);
expect(result.current.showEmptyState).toBe(false);
});
it("should hide search results when loading", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0x123",
isLoading: true,
}),
);
expect(result.current.showSearchResults).toBe(false);
});
it("should show search results when has search value and not loading", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xabc",
isLoading: false,
}),
);
expect(result.current.showSearchResults).toBe(true);
});
it("should set isAddressComplete for valid status", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xvalid",
result: createDefaultResult({ status: "valid" }),
}),
);
expect(result.current.isAddressComplete).toBe(true);
});
it("should set isAddressComplete for ens_resolved status", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "vitalik.eth",
result: createDefaultResult({ status: "ens_resolved" }),
recipientSupportsDomain: true,
}),
);
expect(result.current.isAddressComplete).toBe(true);
});
it("should set isAddressComplete for sanctioned status", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xsanctioned",
result: createDefaultResult({ status: "sanctioned" }),
}),
);
expect(result.current.isAddressComplete).toBe(true);
expect(result.current.isSanctioned).toBe(true);
});
it("should not set isAddressComplete for idle or invalid status", () => {
const { result: idleResult } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0x",
result: createDefaultResult({ status: "idle" }),
}),
);
expect(idleResult.current.isAddressComplete).toBe(false);
const { result: invalidResult } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "bad",
result: createDefaultResult({ status: "invalid" }),
}),
);
expect(invalidResult.current.isAddressComplete).toBe(false);
});
it("should show sanctioned banner when address is sanctioned and has search value", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xsanctioned",
result: createDefaultResult({ status: "sanctioned" }),
}),
);
expect(result.current.showSanctionedBanner).toBe(true);
expect(result.current.isSanctioned).toBe(true);
});
it("should show address validation error when result has error and no matches", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "invalid",
result: createDefaultResult({
status: "invalid",
error: "incorrect_format",
}),
}),
);
expect(result.current.showAddressValidationError).toBe(true);
expect(result.current.addressValidationErrorType).toBe("incorrect_format");
});
it("should show address validation error for bridge InvalidAddress and return incorrect_format when no dot", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "invalidaddr",
result: createDefaultResult({
status: "valid",
bridgeErrors: { recipient: new InvalidAddress() },
}),
}),
);
expect(result.current.showAddressValidationError).toBe(true);
expect(result.current.addressValidationErrorType).toBe("incorrect_format");
});
it("should return wallet_not_exist when bridge InvalidAddress and search contains dot", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "foo.bar.eth",
result: createDefaultResult({
status: "valid",
bridgeErrors: { recipient: new InvalidAddress() },
}),
}),
);
expect(result.current.addressValidationErrorType).toBe("wallet_not_exist");
});
it("should not treat InvalidAddressBecauseDestinationIsAlsoSource as bridge invalid address", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xself",
result: createDefaultResult({
status: "valid",
bridgeErrors: {
recipient: new InvalidAddressBecauseDestinationIsAlsoSource(),
},
}),
}),
);
expect(result.current.showAddressValidationError).toBe(false);
expect(result.current.bridgeRecipientError).toBeInstanceOf(
InvalidAddressBecauseDestinationIsAlsoSource,
);
});
it("should show matched address when result is valid with no error", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xvalid",
result: createDefaultResult({
status: "valid",
error: null,
}),
}),
);
expect(result.current.showMatchedAddress).toBe(true);
});
it("should show matched address when has matchedRecentAddress", () => {
const recent = {
address: "0xrecent",
currency: {} as never,
lastUsedAt: new Date(),
};
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xrecent",
result: createDefaultResult({
status: "valid",
matchedRecentAddress: recent,
}),
}),
);
expect(result.current.showMatchedAddress).toBe(true);
});
it("should show matched address when isLedgerAccount", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xledger",
result: createDefaultResult({
status: "valid",
isLedgerAccount: true,
}),
}),
);
expect(result.current.showMatchedAddress).toBe(true);
});
it("should show empty state when search has value but no matches and no error", () => {
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xunknown",
result: createDefaultResult({
status: "idle",
matchedAccounts: [],
matchedRecentAddress: undefined,
ensName: undefined,
isLedgerAccount: false,
}),
}),
);
expect(result.current.showEmptyState).toBe(true);
expect(result.current.showMatchedAddress).toBe(false);
expect(result.current.showAddressValidationError).toBe(false);
});
it("should expose bridge recipient warning", () => {
const warning = new Error("Low balance");
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xwarn",
result: createDefaultResult({
status: "valid",
bridgeWarnings: { recipient: warning },
}),
}),
);
expect(result.current.bridgeRecipientWarning).toBe(warning);
});
it("should show bridge sender error when present", () => {
const senderError = new Error("Insufficient balance");
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xaddr",
result: createDefaultResult({
status: "valid",
bridgeErrors: { sender: senderError },
}),
}),
);
expect(result.current.bridgeSenderError).toBe(senderError);
expect(result.current.showBridgeSenderError).toBe(true);
});
it("should show bridge recipient error when no matches and not invalid address", () => {
const recipientError = new Error("Bridge recipient error");
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xaddr",
result: createDefaultResult({
status: "valid",
matchedAccounts: [],
matchedRecentAddress: undefined,
ensName: undefined,
isLedgerAccount: false,
bridgeErrors: { recipient: recipientError },
}),
}),
);
expect(result.current.showBridgeRecipientError).toBe(true);
expect(result.current.bridgeRecipientError).toBe(recipientError);
});
it("should show bridge recipient error even when a recent address matches", () => {
const recipientError = new InvalidAddressBecauseDestinationIsAlsoSource();
const recent = {
address: "tz1self",
currency: {} as never,
lastUsedAt: new Date(),
};
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "tz1self",
result: createDefaultResult({
status: "valid",
matchedRecentAddress: recent,
bridgeErrors: { recipient: recipientError },
}),
}),
);
expect(result.current.showMatchedAddress).toBe(true);
expect(result.current.showBridgeRecipientError).toBe(true);
expect(result.current.bridgeRecipientError).toBe(recipientError);
});
it("should show bridge recipient warning when no recipient error and no address validation error", () => {
const warning = new Error("Warning");
const { result } = renderHook(() =>
useRecipientSearchState({
...defaultProps,
searchValue: "0xvalid",
result: createDefaultResult({
status: "valid",
bridgeWarnings: { recipient: warning },
}),
}),
);
expect(result.current.showBridgeRecipientWarning).toBe(true);
});
});