-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0e45db2
commit f07c5fc
Showing
5 changed files
with
376 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.