@lukasrosario/account
Version:
Base Account SDK
214 lines (178 loc) • 6.08 kB
text/typescript
import { Mock, vi } from 'vitest';
import { AppMetadata, Preference } from ':core/provider/interface.js';
import { CB_KEYS_URL, PACKAGE_NAME, PACKAGE_VERSION } from ':core/constants.js';
import { openPopup } from ':util/web.js';
import { Message, MessageID } from '../message/Message.js';
import { Communicator } from './Communicator.js';
vi.mock(':util/web', () => ({
openPopup: vi.fn(),
}));
// Dispatches a message event to simulate postMessage calls from the popup
function dispatchMessageEvent({ data, origin }: { data: Record<string, any>; origin: string }) {
const messageEvent = new MessageEvent('message', {
data,
origin,
});
window.dispatchEvent(messageEvent);
}
const popupLoadedMessage = {
data: { event: 'PopupLoaded' },
};
/**
* Queues a message event to be dispatched after a delay.
*
* This is used to simulate messages dispatched by the popup. Because there is
* no event emitted by the SDK to denote whether it's ready to receive, this
* leverages a simple timeout of 200ms.
*/
function queueMessageEvent({
data,
origin = new URL(CB_KEYS_URL).origin,
}: {
data: Record<string, any>;
origin?: string;
}) {
setTimeout(() => dispatchMessageEvent({ data, origin }), 200);
}
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const appMetadata: AppMetadata = {
appName: 'Test App',
appLogoUrl: null,
appChainIds: [1],
};
const preference: Preference = { walletUrl: CB_KEYS_URL };
describe('Communicator', () => {
let urlOrigin: string;
let communicator: Communicator;
let mockPopup: Pick<
Exclude<Communicator['popup'], null>,
'postMessage' | 'close' | 'closed' | 'focus'
>;
beforeEach(() => {
vi.clearAllMocks();
// url defaults to CB_KEYS_URL
communicator = new Communicator({
url: CB_KEYS_URL,
metadata: appMetadata,
preference,
});
urlOrigin = new URL(CB_KEYS_URL).origin;
mockPopup = {
postMessage: vi.fn(),
close: vi.fn(),
closed: false,
focus: vi.fn(),
} as unknown as Window;
(openPopup as Mock).mockImplementation(() => mockPopup);
});
describe('onMessage', () => {
it('should add and remove event listener', async () => {
const mockRequest: Message = {
requestId: 'mock-request-id-1-2',
data: 'test',
};
queueMessageEvent({ data: mockRequest });
const promise = communicator.onMessage(() => true);
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
expect(await promise).toEqual(mockRequest);
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
});
});
describe('postRequestAndWaitForResponse', () => {
it('should post a message to the popup window and wait for response', async () => {
const mockRequest: Message & { id: MessageID } = { id: 'mock-request-id-1-2', data: {} };
queueMessageEvent(popupLoadedMessage);
queueMessageEvent({
data: {
requestId: mockRequest.id,
},
});
const response = await communicator.postRequestAndWaitForResponse(mockRequest);
expect(mockPopup.postMessage).toHaveBeenNthCalledWith(
1,
{
data: {
version: PACKAGE_VERSION,
sdkName: PACKAGE_NAME,
metadata: appMetadata,
preference,
location: 'http://localhost:3000/',
},
},
urlOrigin
);
expect(mockPopup.postMessage).toHaveBeenNthCalledWith(2, mockRequest, urlOrigin);
expect(response).toEqual({
requestId: mockRequest.id,
});
});
});
describe('postMessage', () => {
it('should post a response to the popup window', async () => {
const mockResponse: Message = { requestId: 'mock-request-id-1-2', data: {} };
queueMessageEvent(popupLoadedMessage);
await communicator.postMessage(mockResponse);
expect(mockPopup.postMessage).toHaveBeenNthCalledWith(
1,
{
data: {
version: PACKAGE_VERSION,
sdkName: PACKAGE_NAME,
metadata: appMetadata,
preference,
location: 'http://localhost:3000/',
},
},
urlOrigin
);
expect(mockPopup.postMessage).toHaveBeenNthCalledWith(2, mockResponse, urlOrigin);
});
});
describe('waitForPopupLoaded', () => {
it('should open a popup window and finish handshake', async () => {
queueMessageEvent(popupLoadedMessage);
const popup = await communicator.waitForPopupLoaded();
expect(openPopup).toHaveBeenCalledWith(new URL(CB_KEYS_URL));
expect(mockPopup.postMessage).toHaveBeenNthCalledWith(
1,
{
data: {
version: PACKAGE_VERSION,
sdkName: PACKAGE_NAME,
metadata: appMetadata,
preference,
location: 'http://localhost:3000/',
},
},
urlOrigin
);
expect(popup).toBeTruthy();
});
it('should re-focus and return the existing popup window if one is already open.', async () => {
mockPopup = {
postMessage: vi.fn(),
close: vi.fn(),
closed: false,
focus: vi.fn(),
} as unknown as Window;
(openPopup as Mock).mockImplementationOnce(() => mockPopup);
queueMessageEvent(popupLoadedMessage);
await communicator.waitForPopupLoaded();
expect(mockPopup.focus).toHaveBeenCalledTimes(1);
expect(openPopup).toHaveBeenCalledTimes(1);
});
it('should open a popup window if an existing one is defined but closed', async () => {
mockPopup = {
postMessage: vi.fn(),
close: vi.fn(),
// Simulate the popup being closed
closed: true,
} as unknown as Window;
(openPopup as Mock).mockImplementationOnce(() => mockPopup);
queueMessageEvent(popupLoadedMessage);
await communicator.waitForPopupLoaded();
expect(openPopup).toHaveBeenCalledTimes(2);
});
});
});