Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
sastaachar committed Jan 3, 2025
1 parent 0e45db2 commit f07c5fc
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 4 deletions.
116 changes: 116 additions & 0 deletions src/embed/hostEventClient/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { HostEvent } from '../../types';

export enum UiPassthroughEvent {
addVizToPinboard = 'addVizToPinboard',
saveAnswer = 'saveAnswer',
getA3AnalysisColumns = 'getA3AnalysisColumns',
getPinboardTabInfo = 'getPinboardTabInfo',
getDiscoverabilityStatus = 'getDiscoverabilityStatus',
getAvailableUiPassthroughs = 'getAvailableUiPassthroughs',
getAnswerPageConfig = 'getAnswerPageConfig',
getPinboardPageConfig = 'getPinboardPageConfig',
UiPassthroughEventNotFound = 'UiPassthroughEventNotFound',
}

export type UiPassthroughContractBase = {
[UiPassthroughEvent.addVizToPinboard]: {
request: {
vizId?: string;
newVizName: string;
newVizDescription?: string;
pinboardId?: string;
tabId?: string;
newPinboardName?: string;
newTabName?: string;
pinFromStore?: boolean;
};
response: {
pinboardId: string;
tabId: string;
vizId: string;
errors?: any;
};
};
[UiPassthroughEvent.saveAnswer]: {
request: {
name: string;
description: string;
vizId: string;
isDiscoverable?: boolean;
};
response: {
answerId: string,
errors?: any;
};
};
[UiPassthroughEvent.getA3AnalysisColumns]: {
request: {
vizId?: string;
};
response: {
data?: any;
errors?: any;
};
};
[UiPassthroughEvent.getPinboardTabInfo]: {
request: any;
response: any;
};
[UiPassthroughEvent.getDiscoverabilityStatus]: {
request: any;
response: {
shouldShowDiscoverability: boolean;
isDiscoverabilityCheckboxUnselectedPerOrg: boolean;
};
};
[UiPassthroughEvent.getAvailableUiPassthroughs]: {
request: any;
response: {
keys: string[];
};
};
[UiPassthroughEvent.getAnswerPageConfig]: {
request: {
vizId?: string;
};
response: any;
};
[UiPassthroughEvent.getPinboardPageConfig]: {
request: any;
response: any;
};
[UiPassthroughEvent.UiPassthroughEventNotFound]: {
request: any;
response: any;
};
};

export type UiPassthroughRequest<T extends keyof UiPassthroughContractBase> = UiPassthroughContractBase[T]['request'];
export type UiPassthroughResponse<T extends keyof UiPassthroughContractBase> = UiPassthroughContractBase[T]['response'];

export type UiPassthroughArrayResponse<ApiName extends keyof UiPassthroughContractBase> =
Promise<Array<{
redId?: string;
value?: UiPassthroughArrayResponse<ApiName>;
error?: any;
}>>

export type EmbedApiHostEventMapping = {
[HostEvent.Pin]: UiPassthroughEvent.addVizToPinboard;
[HostEvent.SaveAnswer]: UiPassthroughEvent.saveAnswer;
'hostEventNotMapped': UiPassthroughEvent.UiPassthroughEventNotFound;
}

export type FlattenType<T> = T extends infer R ? { [K in keyof R]: R[K] } : never;

export type HostEventRequest<HostEventT extends HostEvent> =
HostEventT extends keyof EmbedApiHostEventMapping ?
FlattenType<UiPassthroughRequest<EmbedApiHostEventMapping[HostEventT]>> : any;

export type HostEventResponse<HostEventT extends HostEvent> =
HostEventT extends keyof EmbedApiHostEventMapping ?
{
value?: UiPassthroughRequest<EmbedApiHostEventMapping[HostEventT]>
error?: any;
}
: any;
164 changes: 164 additions & 0 deletions src/embed/hostEventClient/host-event-client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { getIFrameEl } from '../../test/test-utils';
import { HostEvent } from '../../types';
import { processTrigger } from '../../utils/processTrigger';
import {
UiPassthroughEvent,
UiPassthroughRequest,
UiPassthroughArrayResponse,
HostEventRequest,
} from './contracts';
import { HostEventClient } from './host-event-client';

jest.mock('../../utils/processTrigger');

const mockProcessTrigger = processTrigger as jest.Mock;

const createHostEventClient = () => {
const mockIframe = document.createElement('iframe');
const mockThoughtSpotHost = 'http://localhost';
const client = new HostEventClient(mockIframe, mockThoughtSpotHost);
return { client, mockIframe, mockThoughtSpotHost };
};

describe('HostEventClient', () => {
afterEach(() => {
jest.clearAllMocks();
});

describe('executeUiPassthroughApi', () => {
it('should call processTrigger with correct parameters and return response', async () => {
const { client, mockIframe, mockThoughtSpotHost } = createHostEventClient();

const apiName = UiPassthroughEvent.addVizToPinboard;
const parameters: UiPassthroughRequest<typeof apiName> = {
newVizName: 'testViz',
};
const triggerResponse = Promise.resolve([
{ value: { pinboardId: 'testPinboard', tabId: 'testTab', vizId: 'testVizId' } },
]);

mockProcessTrigger.mockResolvedValue(triggerResponse);

const result = await client.executeUiPassthroughApi(getIFrameEl(), apiName, parameters);

expect(mockProcessTrigger).toHaveBeenCalledWith(
mockIframe,
HostEvent.UiPassthrough,
mockThoughtSpotHost,
{
type: apiName,
parameters,
},
);
expect(result).toEqual(await triggerResponse);
});
});

describe('handleUiPassthroughForHostEvent', () => {
it('should return the value from the first valid response', async () => {
const { client } = createHostEventClient();
const apiName = UiPassthroughEvent.addVizToPinboard;
const parameters: UiPassthroughRequest<typeof apiName> = {
newVizName: 'testViz',
};
const triggerResponse = Promise.resolve([
{ value: { pinboardId: 'testPinboard', tabId: 'testTab', vizId: 'testVizId' } },
]);
mockProcessTrigger.mockResolvedValue(triggerResponse);

const result = await client.handleUiPassthroughForHostEvent(getIFrameEl(),
apiName, parameters);

expect(result).toEqual({
pinboardId: 'testPinboard',
tabId: 'testTab',
vizId: 'testVizId',
});
});

it('should throw an error if no valid response is found', async () => {
const { client } = createHostEventClient();
const apiName = UiPassthroughEvent.addVizToPinboard;
const parameters: UiPassthroughRequest<typeof apiName> = {
newVizName: 'testViz',
};
const triggerResponse: UiPassthroughArrayResponse<typeof apiName> = Promise.resolve([]);
mockProcessTrigger.mockResolvedValue(triggerResponse);

await expect(client.handleUiPassthroughForHostEvent(getIFrameEl(), apiName, parameters))
.rejects.toThrow('No answer found');
});

it('should throw an error if the response contains errors', async () => {
const { client } = createHostEventClient();
const apiName = UiPassthroughEvent.addVizToPinboard;
const parameters: UiPassthroughRequest<typeof apiName> = {
newVizName: 'testViz',
};
const triggerResponse: UiPassthroughArrayResponse<typeof apiName> = Promise.resolve([
{ error: 'Some error' },
]);
mockProcessTrigger.mockResolvedValue(triggerResponse);

await expect(client.handleUiPassthroughForHostEvent(getIFrameEl(), apiName, parameters))
.rejects.toThrow(JSON.stringify({ errors: 'Some error' }));
});
});

describe('executeHostEvent', () => {
it('should call handleUiPassthroughForHostEvent for Pin event', async () => {
const { client } = createHostEventClient();
const hostEvent = HostEvent.Pin;
const payload: HostEventRequest<typeof hostEvent> = {
newVizName: 'testViz',
};
const mockResponse = {
pinboardId: 'testPinboard',
tabId: 'testTab',
vizId: 'testVizId',
};
jest.spyOn(client, 'handleUiPassthroughForHostEvent').mockResolvedValue(mockResponse);

const result = await client.executeHostEvent(getIFrameEl(), hostEvent, payload);

expect(client.handleUiPassthroughForHostEvent).toHaveBeenCalledWith(
UiPassthroughEvent.addVizToPinboard,
payload,
);
expect(result).toEqual(mockResponse);
});

it('should call handleUiPassthroughForHostEvent for SaveAnswer event', async () => {
const { client } = createHostEventClient();
const hostEvent = HostEvent.SaveAnswer;
const payload: HostEventRequest<typeof hostEvent> = {
name: 'Test Answer',
description: 'Test Description',
vizId: 'testVizId',
};
const mockResponse = { answerId: 'testAnswerId' };
jest.spyOn(client, 'handleUiPassthroughForHostEvent').mockResolvedValue(mockResponse);

const result = await client.executeHostEvent(getIFrameEl(), hostEvent, payload);

expect(client.handleUiPassthroughForHostEvent).toHaveBeenCalledWith(
UiPassthroughEvent.saveAnswer,
payload,
);
expect(result).toEqual(mockResponse);
});

it('should call hostEventFallback for unmapped events', async () => {
const { client } = createHostEventClient();
const hostEvent = 'testEvent' as HostEvent;
const payload = { data: 'testData' };
const mockResponse = { fallbackResponse: 'data' };
jest.spyOn(client, 'hostEventFallback').mockResolvedValue(mockResponse);

const result = await client.executeHostEvent(getIFrameEl(), hostEvent, payload);

expect(client.hostEventFallback).toHaveBeenCalledWith(hostEvent, payload);
expect(result).toEqual(mockResponse);
});
});
});
72 changes: 72 additions & 0 deletions src/embed/hostEventClient/host-event-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { HostEvent } from '../../types';
import { processTrigger } from '../../utils/processTrigger';
import {
UiPassthroughArrayResponse,
UiPassthroughEvent, HostEventRequest, HostEventResponse,
UiPassthroughRequest,
} from './contracts';

export class HostEventClient {
thoughtSpotHost: string;

constructor(iFrame: HTMLIFrameElement, thoughtSpotHost: string) {
this.thoughtSpotHost = thoughtSpotHost;
}

async executeUiPassthroughApi(iFrame: HTMLIFrameElement, apiName: UiPassthroughEvent,
parameters: UiPassthroughRequest<UiPassthroughEvent>):
UiPassthroughArrayResponse<UiPassthroughEvent> {
const res = await processTrigger(iFrame, HostEvent.UiPassthrough, this.thoughtSpotHost, {
type: apiName,
parameters,
});

return res;
}

async handleUiPassthroughForHostEvent(
iFrame: HTMLIFrameElement,
apiName: UiPassthroughEvent,
parameters: UiPassthroughRequest<UiPassthroughEvent>,
):
UiPassthroughArrayResponse<UiPassthroughEvent> {
const response = (await this.executeUiPassthroughApi(iFrame, apiName, parameters))
?.filter?.((r) => r.error || r.value)[0];

if (!response) {
throw new Error('No answer found');
}

const errors = response.error || (response.value as any)?.errors;
if (errors) {
throw new Error(JSON.stringify({ errors: response.error }));
}

return { ...response.value };
}

async hostEventFallback(
iFrame: HTMLIFrameElement, hostEvent: HostEvent, data: any,
): Promise<any> {
return processTrigger(iFrame, hostEvent, this.thoughtSpotHost, data);
}

async executeHostEvent<T extends HostEvent>(
iFrame:HTMLIFrameElement, hostEvent: HostEvent, payload?: HostEventRequest<T>,
):
Promise<HostEventResponse<HostEvent>> {
if (hostEvent === HostEvent.Pin && typeof payload === 'object') {
return this.handleUiPassthroughForHostEvent(
iFrame,
UiPassthroughEvent.addVizToPinboard, payload,
);
}

if (hostEvent === HostEvent.SaveAnswer && typeof payload === 'object') {
return this.handleUiPassthroughForHostEvent(iFrame,
UiPassthroughEvent.saveAnswer, payload);
}

return this.hostEventFallback(iFrame, hostEvent, payload);
}
}
Loading

0 comments on commit f07c5fc

Please sign in to comment.