@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
text/typescript
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();
});
});
});