UNPKG

@frak-labs/core-sdk

Version:

Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.

479 lines (388 loc) 15.7 kB
import { beforeEach, describe, expect, test, vi } from "vitest"; // Mock dependencies vi.mock("@frak-labs/frame-connector", () => ({ Deferred: class { promise: Promise<any>; resolve!: (value: any) => void; reject!: (error: any) => void; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } }, })); vi.mock("../../constants", () => ({ BACKUP_KEY: "frak-backup-key", })); vi.mock("../../utils/iframe/iframeHelper", () => ({ changeIframeVisibility: vi.fn(), })); vi.mock("../../config/clientId", () => ({ getClientId: vi.fn(() => "mock-client-id"), })); vi.mock("../../utils/browser/deepLinkWithFallback", () => ({ isFrakDeepLink: vi.fn((url: string) => url.startsWith("frakwallet://")), triggerDeepLinkWithFallback: vi.fn(), })); const WALLET_ORIGIN = "https://wallet.frak.id"; describe("createIFrameLifecycleManager", () => { beforeEach(() => { vi.clearAllMocks(); // Reset localStorage localStorage.clear(); // Mock window.location Object.defineProperty(window, "location", { value: { href: "https://test.com" }, writable: true, }); }); describe("manager initialization", () => { test("should create manager with correct properties", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); expect(manager).toBeDefined(); expect(manager.isConnected).toBeInstanceOf(Promise); expect(manager.handleEvent).toBeInstanceOf(Function); }); test("should start with unresolved isConnected promise", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); let resolved = false; manager.isConnected.then(() => { resolved = true; }); // Wait a tick await new Promise((resolve) => setTimeout(resolve, 0)); expect(resolved).toBe(false); }); }); describe("connected event", () => { test("should resolve isConnected on connected event", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "connected" as const, }; manager.handleEvent(event); await expect(manager.isConnected).resolves.toBe(true); }); }); describe("backup events", () => { test("should save backup to localStorage on do-backup", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const backup = "encrypted-backup-data"; const event = { iframeLifecycle: "do-backup" as const, data: { backup }, }; manager.handleEvent(event); expect(localStorage.getItem("frak-backup-key")).toBe(backup); }); test("should remove backup when data.backup is undefined", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); // First set a backup localStorage.setItem("frak-backup-key", "old-backup"); const event = { iframeLifecycle: "do-backup" as const, data: {}, }; manager.handleEvent(event); expect(localStorage.getItem("frak-backup-key")).toBeNull(); }); test("should remove backup on remove-backup event", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); // First set a backup localStorage.setItem("frak-backup-key", "backup-to-remove"); const event = { iframeLifecycle: "remove-backup" as const, }; manager.handleEvent(event); expect(localStorage.getItem("frak-backup-key")).toBeNull(); }); }); describe("visibility events", () => { test("should show iframe on show event", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { changeIframeVisibility } = await import( "../../utils/iframe/iframeHelper" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "show" as const, }; manager.handleEvent(event); expect(changeIframeVisibility).toHaveBeenCalledWith({ iframe: mockIframe, isVisible: true, }); }); test("should hide iframe on hide event", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { changeIframeVisibility } = await import( "../../utils/iframe/iframeHelper" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "hide" as const, }; manager.handleEvent(event); expect(changeIframeVisibility).toHaveBeenCalledWith({ iframe: mockIframe, isVisible: false, }); }); }); describe("redirect event", () => { test("should redirect with appended current URL for HTTP URLs", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); Object.defineProperty(window, "location", { value: { href: "https://original.com", }, writable: true, }); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "redirect" as const, data: { baseRedirectUrl: "https://redirect.com?u=placeholder", }, }; manager.handleEvent(event); expect(window.location.href).toBe( "https://redirect.com/?u=https%3A%2F%2Foriginal.com" ); }); test("should redirect without modification if no u parameter", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); Object.defineProperty(window, "location", { value: { href: "https://original.com", }, writable: true, }); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "redirect" as const, data: { baseRedirectUrl: "https://redirect.com/path", }, }; manager.handleEvent(event); expect(window.location.href).toBe("https://redirect.com/path"); }); test("should use fallback detection for frakwallet:// deep links", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { triggerDeepLinkWithFallback } = await import( "../../utils/browser/deepLinkWithFallback" ); Object.defineProperty(window, "location", { value: { href: "https://original.com", }, writable: true, }); const mockIframe = document.createElement("iframe"); mockIframe.src = "https://wallet.frak.id"; const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "redirect" as const, data: { baseRedirectUrl: "frakwallet://wallet", }, }; manager.handleEvent(event); expect(triggerDeepLinkWithFallback).toHaveBeenCalledWith( "frakwallet://wallet", expect.objectContaining({ onFallback: expect.any(Function), }) ); }); test("should post deep-link-failed message when fallback is triggered", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { triggerDeepLinkWithFallback } = await import( "../../utils/browser/deepLinkWithFallback" ); Object.defineProperty(window, "location", { value: { href: "https://original.com", }, writable: true, }); const mockPostMessage = vi.fn(); const mockIframe = { src: "https://wallet.frak.id", contentWindow: { postMessage: mockPostMessage, }, } as any; const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "redirect" as const, data: { baseRedirectUrl: "frakwallet://wallet", }, }; manager.handleEvent(event); // Extract the onFallback callback from the mock call const callArgs = (triggerDeepLinkWithFallback as any).mock.calls[0]; const options = callArgs[1]; expect(options).toBeDefined(); expect(options.onFallback).toBeInstanceOf(Function); // Trigger the fallback callback options.onFallback(); // Verify postMessage was called with deep-link-failed event expect(mockPostMessage).toHaveBeenCalledWith( { clientLifecycle: "deep-link-failed", data: { originalUrl: "frakwallet://wallet" }, }, "https://wallet.frak.id" ); }); test("should NOT use fallback detection for HTTP URLs", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { triggerDeepLinkWithFallback } = await import( "../../utils/browser/deepLinkWithFallback" ); Object.defineProperty(window, "location", { value: { href: "https://original.com", }, writable: true, }); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { iframeLifecycle: "redirect" as const, data: { baseRedirectUrl: "https://wallet.frak.id/login", }, }; manager.handleEvent(event); // Should NOT call fallback detection expect(triggerDeepLinkWithFallback).not.toHaveBeenCalled(); // Should directly redirect expect(window.location.href).toBe("https://wallet.frak.id/login"); }); }); describe("event filtering", () => { test("should ignore events without iframeLifecycle property", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); const event = { someOtherEvent: "value", } as any; // Should not throw expect(manager.handleEvent(event)).toBeUndefined(); }); test("should only process events with iframeLifecycle", async () => { const { createIFrameLifecycleManager } = await import( "./iframeLifecycleManager" ); const { changeIframeVisibility } = await import( "../../utils/iframe/iframeHelper" ); const mockIframe = document.createElement("iframe"); const manager = createIFrameLifecycleManager({ iframe: mockIframe, targetOrigin: WALLET_ORIGIN, }); // Event without iframeLifecycle manager.handleEvent({ randomEvent: "show" } as any); // changeIframeVisibility should not be called expect(changeIframeVisibility).not.toHaveBeenCalled(); // Event with iframeLifecycle manager.handleEvent({ iframeLifecycle: "show" as const }); // Now it should be called expect(changeIframeVisibility).toHaveBeenCalled(); }); }); });