UNPKG

@ledgerhq/live-common

Version:
345 lines (297 loc) • 11.3 kB
/** * @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; }