@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
345 lines (297 loc) • 11.3 kB
text/typescript
/**
* @jest-environment jsdom
*/
if (typeof globalThis.setImmediate !== "function") {
// Force React scheduler to avoid MessageChannel in jsdom + detectOpenHandles.
// @ts-expect-error Test-only polyfill for environments without setImmediate.
globalThis.setImmediate = (callback: (...args: unknown[]) => void, ...args: unknown[]) => {
setTimeout(() => callback(...args), 0);
};
}
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const { renderHook, act, cleanup } = require("@testing-library/react");
import { initialState as walletState } from "@ledgerhq/live-wallet/store";
import { createFixtureAccount } from "../mock/fixtures/cryptoCurrencies";
import type { TrackingAPI } from "./tracking";
import type { useWalletAPIServerOptions } from "./react";
const mockSetHandler = jest.fn();
const mockSetConfig = jest.fn();
const mockSetPermissions = jest.fn();
const mockSetCustomHandlers = jest.fn();
jest.mock("@ledgerhq/wallet-api-server", () => ({
WalletAPIServer: jest.fn().mockImplementation(() => ({
setHandler: mockSetHandler,
setConfig: mockSetConfig,
setPermissions: mockSetPermissions,
setCustomHandlers: mockSetCustomHandlers,
})),
}));
jest.mock("react-redux", () => ({
useDispatch: jest.fn().mockReturnValue(jest.fn()),
}));
jest.mock("../featureFlags/FeatureFlagsContext", () => ({
useFeatureFlags: jest.fn().mockReturnValue({
isFeature: () => true,
getFeature: () => null,
overrideFeature: jest.fn(),
resetFeature: jest.fn(),
resetFeatures: jest.fn(),
}),
}));
jest.mock("../modularDrawer/hooks/useCurrenciesUnderFeatureFlag", () => ({
useCurrenciesUnderFeatureFlag: jest.fn().mockReturnValue({
featureFlaggedCurrencies: {},
deactivatedCurrencyIds: new Set(),
}),
}));
jest.mock("./FeatureFlags", () => ({
handlers: jest.fn().mockReturnValue({ "custom.featureFlags.get": jest.fn() }),
}));
jest.mock("./converters", () => ({
...jest.requireActual("./converters"),
setWalletApiIdForAccountId: jest.fn(),
}));
jest.mock("../hw/openTransportAsSubject", () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock("@ledgerhq/cryptoassets/cal-client/state-manager/api", () => ({
endpoints: {
getTokensData: { initiate: jest.fn() },
},
}));
jest.mock("@ledgerhq/cryptoassets/state", () => ({
getCryptoAssetsStore: jest.fn().mockReturnValue({
findTokenById: jest.fn().mockResolvedValue(null),
}),
}));
jest.mock("../currencies", () => ({
listSupportedCurrencies: jest.fn().mockReturnValue([]),
}));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { useWalletAPIServer } = require("./react");
function createDefaultOptions(
overrides?: Partial<useWalletAPIServerOptions>,
): useWalletAPIServerOptions {
const tracking = createMockTracking();
return {
walletState,
manifest: {
id: "test-app",
private: false,
name: "Test App",
url: "https://test.app",
homepageUrl: "https://test.app",
supportUrl: "https://test.app",
icon: null,
platforms: ["desktop"],
apiVersion: "1.0.0",
manifestVersion: "1.0.0",
branch: "debug",
params: undefined,
categories: [],
currencies: "*",
content: {
shortDescription: { en: "test" },
description: { en: "test" },
},
permissions: [],
domains: [],
visibility: "complete" as const,
},
accounts: [createFixtureAccount("01"), createFixtureAccount("02")],
tracking,
config: {
appId: "test-app-id",
userId: "test-user-id",
tracking: false,
wallet: { name: "ledger-live-desktop", version: "1.0.0" },
},
webviewHook: {
reload: jest.fn(),
postMessage: jest.fn(),
},
uiHook: {},
...overrides,
};
}
describe("useWalletAPIServer", () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it("should return the expected shape", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
expect(result.current).toHaveProperty("server");
expect(result.current).toHaveProperty("onMessage");
expect(result.current).toHaveProperty("onLoad");
expect(result.current).toHaveProperty("onReload");
expect(result.current).toHaveProperty("onLoadError");
expect(result.current).toHaveProperty("widgetLoaded");
expect(typeof result.current.onMessage).toBe("function");
expect(typeof result.current.onLoad).toBe("function");
expect(typeof result.current.onReload).toBe("function");
expect(typeof result.current.onLoadError).toBe("function");
});
it("should start with widgetLoaded as false", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
expect(result.current.widgetLoaded).toBe(false);
});
it("should set widgetLoaded to true when onLoad is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onLoad();
});
expect(result.current.widgetLoaded).toBe(true);
});
it("should track load success when onLoad is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onLoad();
});
expect(options.tracking.loadSuccess).toHaveBeenCalledWith(options.manifest);
});
it("should reset widgetLoaded to false when onReload is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onLoad();
});
expect(result.current.widgetLoaded).toBe(true);
act(() => {
result.current.onReload();
});
expect(result.current.widgetLoaded).toBe(false);
});
it("should call webviewHook.reload when onReload is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onReload();
});
expect(options.webviewHook.reload).toHaveBeenCalledTimes(1);
});
it("should track reload when onReload is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onReload();
});
expect(options.tracking.reload).toHaveBeenCalledWith(options.manifest);
});
it("should track load failure when onLoadError is called", () => {
const options = createDefaultOptions();
const { result } = renderHook(() => useWalletAPIServer(options));
act(() => {
result.current.onLoadError();
});
expect(options.tracking.loadFail).toHaveBeenCalledWith(options.manifest);
});
it("should track load on mount", () => {
const options = createDefaultOptions();
renderHook(() => useWalletAPIServer(options));
expect(options.tracking.load).toHaveBeenCalledWith(options.manifest);
});
it("should register handlers on the server", () => {
const options = createDefaultOptions();
renderHook(() => useWalletAPIServer(options));
const registeredHandlers = mockSetHandler.mock.calls.map(([name]) => name);
expect(registeredHandlers).toContain("currency.list");
expect(registeredHandlers).toContain("account.list");
expect(registeredHandlers).toContain("bitcoin.getAddress");
expect(registeredHandlers).toContain("bitcoin.getAddresses");
expect(registeredHandlers).toContain("bitcoin.getPublicKey");
expect(registeredHandlers).toContain("bitcoin.getXPub");
expect(registeredHandlers).toContain("device.open");
expect(registeredHandlers).toContain("device.exchange");
expect(registeredHandlers).toContain("device.close");
});
it("should register optional handlers only when uiHook callbacks are provided", () => {
const uiAccountRequest = jest.fn();
const options = createDefaultOptions({
uiHook: { "account.request": uiAccountRequest },
});
renderHook(() => useWalletAPIServer(options));
const registeredHandlers = mockSetHandler.mock.calls.map(([name]) => name);
expect(registeredHandlers).toContain("account.request");
});
it("should not register account.request handler when uiHook callback is missing", () => {
const options = createDefaultOptions({ uiHook: {} });
renderHook(() => useWalletAPIServer(options));
const registeredHandlers = mockSetHandler.mock.calls.map(([name]) => name);
expect(registeredHandlers).not.toContain("account.request");
});
});
function createMockTracking(): TrackingAPI {
return {
load: jest.fn(),
reload: jest.fn(),
loadFail: jest.fn(),
loadSuccess: jest.fn(),
signTransactionRequested: jest.fn(),
signTransactionFail: jest.fn(),
signTransactionSuccess: jest.fn(),
signRawTransactionRequested: jest.fn(),
signRawTransactionFail: jest.fn(),
signRawTransactionSuccess: jest.fn(),
requestAccountRequested: jest.fn(),
requestAccountFail: jest.fn(),
requestAccountSuccess: jest.fn(),
receiveRequested: jest.fn(),
receiveFail: jest.fn(),
receiveSuccess: jest.fn(),
broadcastFail: jest.fn(),
broadcastSuccess: jest.fn(),
broadcastOperationDetailsClick: jest.fn(),
startExchangeRequested: jest.fn(),
startExchangeSuccess: jest.fn(),
startExchangeFail: jest.fn(),
completeExchangeRequested: jest.fn(),
completeExchangeSuccess: jest.fn(),
completeExchangeFail: jest.fn(),
signMessageRequested: jest.fn(),
signMessageSuccess: jest.fn(),
signMessageFail: jest.fn(),
signMessageUserRefused: jest.fn(),
deviceTransportRequested: jest.fn(),
deviceTransportSuccess: jest.fn(),
deviceTransportFail: jest.fn(),
deviceSelectRequested: jest.fn(),
deviceSelectSuccess: jest.fn(),
deviceSelectFail: jest.fn(),
deviceOpenRequested: jest.fn(),
deviceExchangeRequested: jest.fn(),
deviceExchangeSuccess: jest.fn(),
deviceExchangeFail: jest.fn(),
deviceCloseRequested: jest.fn(),
deviceCloseSuccess: jest.fn(),
deviceCloseFail: jest.fn(),
bitcoinFamilyAccountAddressRequested: jest.fn(),
bitcoinFamilyAccountAddressFail: jest.fn(),
bitcoinFamilyAccountAddressSuccess: jest.fn(),
bitcoinFamilyAccountPublicKeyRequested: jest.fn(),
bitcoinFamilyAccountPublicKeyFail: jest.fn(),
bitcoinFamilyAccountPublicKeySuccess: jest.fn(),
bitcoinFamilyAccountXpubRequested: jest.fn(),
bitcoinFamilyAccountXpubFail: jest.fn(),
bitcoinFamilyAccountXpubSuccess: jest.fn(),
bitcoinFamilyAccountAddressesRequested: jest.fn(),
bitcoinFamilyAccountAddressesFail: jest.fn(),
bitcoinFamilyAccountAddressesSuccess: jest.fn(),
dappSendTransactionRequested: jest.fn(),
dappSendTransactionSuccess: jest.fn(),
dappSendTransactionFail: jest.fn(),
dappPersonalSignRequested: jest.fn(),
dappPersonalSignSuccess: jest.fn(),
dappPersonalSignFail: jest.fn(),
dappSignTypedDataRequested: jest.fn(),
dappSignTypedDataSuccess: jest.fn(),
dappSignTypedDataFail: jest.fn(),
} as unknown as TrackingAPI;
}