UNPKG

@thoughtspot/visual-embed-sdk

Version:
1,280 lines (1,180 loc) 170 kB
import { resetValueFromWindow } from '../utils'; import { ERROR_MESSAGE } from '../errors'; import { resetCachedAuthToken } from '../authToken'; import { AuthType, init, EmbedEvent, SearchEmbed, PinboardEmbed, LiveboardViewConfig, AppEmbed, LiveboardEmbed, AppViewConfig, SageEmbed, SageViewConfig, SearchViewConfig, AnswerService, } from '../index'; import { Action, HomeLeftNavItem, RuntimeFilter, RuntimeFilterOp, HomepageModule, HostEvent, RuntimeParameter, Param, ContextMenuTriggerOptions, CustomActionTarget, CustomActionsPosition, DefaultAppInitData, ErrorDetailsTypes, EmbedErrorCodes, } from '../types'; import { executeAfterWait, getDocumentBody, getIFrameEl, getIFrameSrc, getRootEl, postMessageToParent, defaultParamsForPinboardEmbed, waitFor, expectUrlMatchesWithParams, expectUrlToHaveParamsWithValues, mockMessageChannel, createRootEleForEmbed, expectUrlMatch, fixedEncodeURI, } from '../test/test-utils'; import * as config from '../config'; import * as embedConfig from './embedConfig'; import * as tsEmbedInstance from './ts-embed'; import * as mixpanelInstance from '../mixpanel-service'; import * as authInstance from '../auth'; import * as baseInstance from './base'; import { MIXPANEL_EVENT } from '../mixpanel-service'; import * as authService from '../utils/authService'; import { logger } from '../utils/logger'; import { version } from '../../package.json'; import { HiddenActionItemByDefaultForSearchEmbed } from './search'; import { processTrigger } from '../utils/processTrigger'; import { UIPassthroughEvent } from './hostEventClient/contracts'; import * as sessionInfoService from '../utils/sessionInfoService'; import * as authToken from '../authToken'; import * as apiIntercept from '../api-intercept'; jest.mock('../utils/processTrigger'); const mockProcessTrigger = processTrigger as jest.Mock; const mockHandleInterceptEvent = jest.spyOn(apiIntercept, 'handleInterceptEvent'); const defaultViewConfig = { frameParams: { width: 1280, height: 720, }, }; const pinboardId = 'eca215d4-0d2c-4a55-90e3-d81ef6848ae0'; const liveboardId = 'eca215d4-0d2c-4a55-90e3-d81ef6848ae0'; const tabId1 = 'eca215d4-0d2c-4a55-90e3-d81ef6848ae0'; const tabId2 = 'eca215d4-0d2c-4a55-90e3-d81ef6848ae0'; const thoughtSpotHost = 'tshost'; const defaultParamsPost = ''; export const defaultParamsWithoutHiddenActions = `hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=${AuthType.None}&blockNonEmbedFullAppAccess=true`; export const defaultParams = `&${defaultParamsWithoutHiddenActions}&hideAction=[%22${Action.ReportError}%22]`; const hideBydefault = `&hideAction=${fixedEncodeURI( JSON.stringify([Action.ReportError, ...HiddenActionItemByDefaultForSearchEmbed]), )}`; const defaultParamsWithHiddenActions = defaultParamsWithoutHiddenActions + hideBydefault; beforeAll(() => { jest.spyOn(window, 'alert').mockImplementation(() => {}); }); const customisations = { style: { customCSS: {}, }, content: {}, }; const customisationsView = { style: { customCSS: {}, }, content: { strings: { DATA: 'data', }, }, }; const customVariablesForThirdPartyTools = { key1: '!@#', key2: '*%^', }; const getMockAppInitPayload = (data: any) => { const defaultData: DefaultAppInitData = { customisations, authToken: '', hostConfig: undefined, runtimeFilterParams: null, runtimeParameterParams: null, hiddenHomeLeftNavItems: [], hiddenHomepageModules: [], hiddenListColumns: [], customActions: [], reorderedHomepageModules: [], customVariablesForThirdPartyTools, interceptTimeout: undefined, interceptUrls: [], }; return { type: EmbedEvent.APP_INIT, data: { ...defaultData, ...data, }, }; } describe('Unit test case for ts embed', () => { const mockMixPanelEvent = jest.spyOn(mixpanelInstance, 'uploadMixpanelEvent'); beforeEach(() => { document.body.innerHTML = getDocumentBody(); }); afterEach(() => { jest.clearAllMocks(); resetCachedAuthToken(); }); beforeAll(() => { jest.spyOn(authInstance, 'postLoginService').mockResolvedValue(undefined); }); describe('Vaidate iframe properties', () => { beforeAll(() => { init({ thoughtSpotHost: 'tshost', authType: AuthType.None, }); }); test('should set proper allow policies', async () => { // we dont have origin specific policies so just checking if // policies are ending with ; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); await executeAfterWait(() => { const iframe = getIFrameEl(); const policiesAdded = iframe.allow.split(' '); policiesAdded.forEach((policy) => { expect(policy.endsWith(';')).toBe(true); }); }); }); test('should get answer service', async () => { const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); mockProcessTrigger.mockResolvedValue({ session: 'test' }); await executeAfterWait(async () => { expect(await searchEmbed.getAnswerService()).toBeInstanceOf(AnswerService); }); }); test('triggerUIPassThrough with params', async () => { const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); mockProcessTrigger.mockResolvedValue({ session: 'test' }); await executeAfterWait(async () => { const payload = { newVizName: 'test' }; await searchEmbed.triggerUIPassThrough( UIPassthroughEvent.PinAnswerToLiveboard, payload, ); expect(mockProcessTrigger).toHaveBeenCalledWith( getIFrameEl(), HostEvent.UIPassthrough, 'http://tshost', { parameters: payload, type: UIPassthroughEvent.PinAnswerToLiveboard, }, ); }); }); test('Host event with empty param', async () => { const liveboardEmbed = new LiveboardEmbed(getRootEl(), { liveboardId: '123', ...defaultViewConfig, }); liveboardEmbed.render(); mockProcessTrigger.mockResolvedValue({ session: 'test' }); await executeAfterWait(async () => { await liveboardEmbed.trigger( HostEvent.Save, ); expect(mockProcessTrigger).toHaveBeenCalledWith( getIFrameEl(), HostEvent.Save, 'http://tshost', {}, ); }); }); test('Host event with falsy param', async () => { const liveboardEmbed = new LiveboardEmbed(getRootEl(), { liveboardId: '123', ...defaultViewConfig, }); liveboardEmbed.render(); mockProcessTrigger.mockResolvedValue({ session: 'test' }); await executeAfterWait(async () => { await liveboardEmbed.trigger( HostEvent.Save, false, ); expect(mockProcessTrigger).toHaveBeenCalledWith( getIFrameEl(), HostEvent.Save, 'http://tshost', false, ); }); }); test('should set proper height, width and min-height to iframe', async () => { // we dont have origin specific policies so just checking if // policies are ending with ; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); await executeAfterWait(() => { const iframe = getIFrameEl(); expect(iframe.style.width).toBe(`${defaultViewConfig.frameParams.width}px`); expect(iframe.style.height).toBe(`${defaultViewConfig.frameParams.height}px`); expect(iframe.style.minHeight).toBe(`${defaultViewConfig.frameParams.height}px`); }); }); }); describe('AuthExpire embedEvent in cookieless authentication authType', () => { beforeAll(() => { jest.spyOn(authInstance, 'doCookielessTokenAuth').mockResolvedValueOnce(true); jest.spyOn(authService, 'verifyTokenService').mockResolvedValueOnce(true); init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthTokenCookieless, getAuthToken: () => Promise.resolve('test_auth_token2'), }); }); test('check for new authToken based on getAuthToken function', async () => { const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); jest.spyOn(baseInstance, 'notifyAuthFailure'); jest.spyOn(baseInstance, 'handleAuth'); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(baseInstance.handleAuth).not.toHaveBeenCalled(); expect(mockPort.postMessage).toHaveBeenCalledWith({ type: EmbedEvent.AuthExpire, data: { authToken: 'test_auth_token2' }, }); }); }); test('check for new authToken based on getAuthToken function', async () => { init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthToken, getAuthToken: () => Promise.resolve('test_auth_token2'), autoLogin: true, }); const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); jest.spyOn(baseInstance, 'notifyAuthFailure'); jest.spyOn(baseInstance, 'handleAuth'); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(mockPort.postMessage).not.toHaveBeenCalledWith({ type: EmbedEvent.AuthExpire, data: { authToken: 'test_auth_token2' }, }); expect(baseInstance.handleAuth).toHaveBeenCalled(); }); }); }); describe('Called Embed event status for start and end', () => { beforeAll(() => { init({ thoughtSpotHost: 'tshost', authType: AuthType.None, customizations: customisations, customVariablesForThirdPartyTools, }); }); test('verify Customisations', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); test('verify Customisations from viewConfig', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, customizations: customisationsView, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ customisations: customisationsView, })); }); }); test('hide home page modules from view Config should be part of app_init payload', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockedHiddenHomepageModules: HomepageModule[] = [ HomepageModule.MyLibrary, HomepageModule.Learning, ]; const searchEmbed = new AppEmbed(getRootEl(), { ...defaultViewConfig, hiddenHomepageModules: mockedHiddenHomepageModules, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ hiddenHomepageModules: [HomepageModule.MyLibrary, HomepageModule.Learning], })); }); }); test('customVariablesForThirdPartyTools should be part of the app_init payload', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new AppEmbed(getRootEl(), { ...defaultViewConfig, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); test('Reordering the home page modules from view Config should be part of app_init payload', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockedReorderedHomepageModules: HomepageModule[] = [ HomepageModule.MyLibrary, HomepageModule.Watchlist, ]; const searchEmbed = new AppEmbed(getRootEl(), { ...defaultViewConfig, reorderedHomepageModules: mockedReorderedHomepageModules, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ reorderedHomepageModules: [HomepageModule.MyLibrary, HomepageModule.Watchlist], })); }); }); test('Runtime parameters from view Config should be part of app_init payload when excludeRuntimeParametsfromURL is true', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockRuntimeParameters: RuntimeParameter[] = [ { name: 'color', value: 'blue', }, ]; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, excludeRuntimeParametersfromURL: true, runtimeParameters: mockRuntimeParameters, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ runtimeParameterParams: 'param1=color&paramVal1=blue', })); }); }); test('Runtime filters from view Config should be part of app_init payload when excludeRuntimeFiltersfromURL is true', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockRuntimeFilters: RuntimeFilter[] = [ { columnName: 'color', operator: RuntimeFilterOp.EQ, values: ['blue'], }, ]; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, excludeRuntimeFiltersfromURL: true, runtimeFilters: mockRuntimeFilters, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ runtimeFilterParams: 'col1=color&op1=EQ&val1=blue', })); }); }); test('Runtime filters from view Config should be not part of app_init payload when excludeRuntimeFiltersfromURL is undefined', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockRuntimeFilters: RuntimeFilter[] = [ { columnName: 'color', operator: RuntimeFilterOp.EQ, values: ['blue'], }, ]; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, runtimeFilters: mockRuntimeFilters, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); test('Runtime filters from view Config should not be part of app_init payload when excludeRuntimeFiltersfromURL is false', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockRuntimeFilters: RuntimeFilter[] = [ { columnName: 'color', operator: RuntimeFilterOp.EQ, values: ['blue'], }, ]; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, excludeRuntimeFiltersfromURL: false, runtimeFilters: mockRuntimeFilters, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({})); }); }); test('homeLeftNav from view Config should be part of app_init payload', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const mockedHiddenHomeLeftNavItems: HomeLeftNavItem[] = [ HomeLeftNavItem.Home, HomeLeftNavItem.MonitorSubscription, ]; const searchEmbed = new AppEmbed(getRootEl(), { ...defaultViewConfig, hiddenHomeLeftNavItems: mockedHiddenHomeLeftNavItems, }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ hiddenHomeLeftNavItems: [HomeLeftNavItem.Home, HomeLeftNavItem.MonitorSubscription], })); }); }); test('when Embed event status have start status', (done) => { const mockEmbedEventPayload = { type: EmbedEvent.Save, data: { answerId: '123' }, status: 'start', }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed .on( EmbedEvent.Save, (payload) => { expect(payload).toEqual(mockEmbedEventPayload); done(); }, { start: true }, ) .render(); executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }); }); test('should not called post message, when Embed event status have start and start option as false', () => { const mockEmbedEventPayload = { type: EmbedEvent.Save, data: { answerId: '123' }, status: 'start', }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed .on(EmbedEvent.Save, () => { logger.log('non callable'); }) .render(); executeAfterWait(() => { const iframe = getIFrameEl(); iframe.contentWindow.postMessage = jest.fn(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); expect(iframe.contentWindow.postMessage).toHaveBeenCalledTimes(0); }); }); test('when Embed event status have end status', (done) => { const mockEmbedEventPayload = { type: EmbedEvent.Save, data: { answerId: '123' }, status: 'end', }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed .on(EmbedEvent.Save, (payload) => { expect(payload).toEqual(mockEmbedEventPayload); done(); }) .render(); executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }, 1000); }); test('should not called post message, when Embed event status have end status and start is true', () => { const mockEmbedEventPayload = { type: EmbedEvent.Save, data: { answerId: '123' }, status: 'end', }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed .on( EmbedEvent.Save, () => { logger.log('non callable'); }, { start: true }, ) .render(); executeAfterWait(() => { const iframe = getIFrameEl(); iframe.contentWindow.postMessage = jest.fn(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); expect(iframe.contentWindow.postMessage).toHaveBeenCalledTimes(0); }, 1000); }); test('should remove event listener when called off method', async () => { const mockEmbedEventPayload = { type: EmbedEvent.Save, data: { answerId: '123' }, status: 'end', }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); const mockFn = jest.fn(); searchEmbed.on(EmbedEvent.Save, mockFn).render(); await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }); searchEmbed.off(EmbedEvent.Save, mockFn); await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }); expect(mockFn).toHaveBeenCalledTimes(1); }); }); describe('Appinit embedEvent in cookieless authentication authType', () => { beforeAll(() => { jest.spyOn(authInstance, 'doCookielessTokenAuth').mockResolvedValueOnce(true); init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthTokenCookieless, getAuthToken: () => Promise.resolve('test_auth_token1'), }); }); afterEach(() => { baseInstance.reset(); }); test('check for authToken based on getAuthToken function', async () => { const a = jest.spyOn(authService, 'verifyTokenService'); a.mockResolvedValue(true); // authVerifyMock.mockResolvedValue(true); const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ authToken: 'test_auth_token1', customVariablesForThirdPartyTools: {}, })); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); }); }); describe('StringIDs and StringIDsUrl in customisations', () => { const customisationWithStringIds = { style: { customCSS: {}, }, content: { strings: { Liveboard: 'Dashboard', }, stringIDsUrl: 'https://sample-string-ids-url.com', stringIDs: { 'liveboard.header.title': 'Dashboard name', }, }, }; beforeEach(() => { jest.spyOn(authInstance, 'doCookielessTokenAuth').mockResolvedValueOnce(true); jest.spyOn(authService, 'verifyTokenService').mockResolvedValue(true); init({ thoughtSpotHost: 'tshost', customizations: customisationWithStringIds, authType: AuthType.TrustedAuthTokenCookieless, getAuthToken: () => Promise.resolve('test_auth_token1'), }); }); afterEach(() => { baseInstance.reset(); jest.clearAllMocks(); }); test('should pass stringIDsUrl and stringIDs in customisations during APP_INIT', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); expect(iframe.src).toContain('overrideStringIDsUrl=https://sample-string-ids-url.com'); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ customisations: { content: { strings: { Liveboard: 'Dashboard', }, stringIDsUrl: 'https://sample-string-ids-url.com', stringIDs: { 'liveboard.header.title': 'Dashboard name', }, }, style: { customCSS: {}, customCSSUrl: undefined, }, }, authToken: 'test_auth_token1', customVariablesForThirdPartyTools: {}, })); const customisationContent = mockPort.postMessage.mock.calls[0][0].data.customisations.content; expect(customisationContent.stringIDsUrl) .toBe('https://sample-string-ids-url.com'); expect(customisationContent.stringIDs) .toEqual({ 'liveboard.header.title': 'Dashboard name', }); }); }); test('should allow passing exposeTranslationIDs in viewConfig', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, exposeTranslationIDs: true }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); expect(iframe.src).toContain('exposeTranslationIDs=true'); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); }); }); describe('getDefaultAppInitData with CustomActionsValidationResult', () => { beforeEach(() => { jest.spyOn(authInstance, 'doCookielessTokenAuth').mockResolvedValueOnce(true); jest.spyOn(authService, 'verifyTokenService').mockResolvedValue(true); init({ thoughtSpotHost: 'tshost', authType: AuthType.TrustedAuthTokenCookieless, getAuthToken: () => Promise.resolve('test_auth_token1'), }); }); afterEach(() => { baseInstance.reset(); jest.clearAllMocks(); }); test('should handle valid custom actions and sort them by name in getDefaultAppInitData', async () => { const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; // Create a SearchEmbed with valid custom actions to test // CustomActionsValidationResult const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, customActions: [ { id: 'action1', name: 'Valid Action', target: CustomActionTarget.LIVEBOARD, position: CustomActionsPosition.PRIMARY, metadataIds: { liveboardIds: ['lb123'] } }, { id: 'action2', name: 'Another Valid Action', target: CustomActionTarget.VIZ, position: CustomActionsPosition.MENU, metadataIds: { vizIds: ['viz456'] } } ] }); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).toHaveBeenCalledWith(getMockAppInitPayload({ customisations: { content: {}, style: { customCSS: {}, customCSSUrl: undefined, }, }, authToken: 'test_auth_token1', customActions: [ { id: 'action2', name: 'Another Valid Action', target: CustomActionTarget.VIZ, position: CustomActionsPosition.MENU, metadataIds: { vizIds: ['viz456'] } }, { id: 'action1', name: 'Valid Action', target: CustomActionTarget.LIVEBOARD, position: CustomActionsPosition.PRIMARY, metadataIds: { liveboardIds: ['lb123'] } } ], // Actions should be sorted by name customVariablesForThirdPartyTools: {}, })); // Verify that CustomActionsValidationResult structure is // correct const appInitData = mockPort.postMessage.mock.calls[0][0].data; expect(appInitData.customActions).toHaveLength(2); expect(appInitData.customActions).toEqual( expect.arrayContaining([ expect.objectContaining({ id: 'action1', name: 'Valid Action', target: CustomActionTarget.LIVEBOARD, position: CustomActionsPosition.PRIMARY }), expect.objectContaining({ id: 'action2', name: 'Another Valid Action', target: CustomActionTarget.VIZ, position: CustomActionsPosition.MENU }) ]) ); // Verify actions are sorted by name (alphabetically) expect(appInitData.customActions[0].name).toBe('Another Valid Action'); expect(appInitData.customActions[1].name).toBe('Valid Action'); }); }); }); describe('Token fetch fails in cookieless authentication authType', () => { beforeEach(() => { jest.spyOn(authInstance, 'doCookielessTokenAuth').mockResolvedValueOnce(true); init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthTokenCookieless, getAuthToken: () => Promise.reject(), }); jest.spyOn(logger, 'error').mockImplementation(() => {}); }); afterEach(() => { jest.clearAllMocks(); baseInstance.reset(); }); test('should show login failure message if token failed during app_init', async () => { const a = jest.spyOn(authService, 'verifyTokenService'); a.mockResolvedValue(true); // authVerifyMock.mockResolvedValue(true); const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(mockPort.postMessage).not.toHaveBeenCalled(); expect(getRootEl().innerHTML).toContain('Not logged in'); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); }); test('should show login failure message if token failed during app_init prerender', async () => { const a = jest.spyOn(authService, 'verifyTokenService'); a.mockResolvedValue(true); // authVerifyMock.mockResolvedValue(true); const mockEmbedEventPayload = { type: EmbedEvent.APP_INIT, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, preRenderId: 'test' }); searchEmbed.preRender(); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); const preRenderWrapper = document.getElementById('tsEmbed-pre-render-wrapper-test'); await executeAfterWait(() => { expect(mockPort.postMessage).not.toHaveBeenCalled(); expect(preRenderWrapper.innerHTML).toContain('Not logged in'); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); }); test('should show login failure message if update token failed', async () => { const a = jest.spyOn(authService, 'verifyTokenService'); a.mockResolvedValue(true); // authVerifyMock.mockResolvedValue(true); const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); jest.spyOn(baseInstance, 'notifyAuthFailure'); searchEmbed.render(); const mockPort: any = { postMessage: jest.fn(), }; const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {}); await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); await executeAfterWait(() => { expect(getRootEl().innerHTML).toContain('Not logged in'); expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(loggerSpy).toHaveBeenCalledTimes(1); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); jest.spyOn(baseInstance, 'notifyAuthFailure').mockClear(); }); test('should show login failure message if update token failed prerender', async () => { const a = jest.spyOn(authService, 'verifyTokenService'); a.mockResolvedValue(true); // authVerifyMock.mockResolvedValue(true); const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), { ...defaultViewConfig, preRenderId: 'test' }); jest.spyOn(baseInstance, 'notifyAuthFailure'); searchEmbed.preRender(); const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {}); const mockPort: any = { postMessage: jest.fn(), }; await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort); }); const preRenderWrapper = document.getElementById('tsEmbed-pre-render-wrapper-test'); await executeAfterWait(() => { expect(preRenderWrapper.innerHTML).toContain('Not logged in'); expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(loggerSpy).toHaveBeenCalledTimes(1); }); jest.spyOn(authService, 'verifyTokenService').mockClear(); jest.spyOn(baseInstance, 'notifyAuthFailure').mockClear(); }); }); xdescribe('AuthExpire embedEvent in TrustedAuthToken authType', () => { test('AutoLogin true scenario', async () => { init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthToken, username: 'tsadmin', getAuthToken: () => Promise.resolve('test_auth_token3'), autoLogin: true, }); const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); jest.spyOn(baseInstance, 'notifyAuthFailure'); jest.spyOn(baseInstance, 'handleAuth'); searchEmbed.render(); await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }); await executeAfterWait(() => { expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(baseInstance.handleAuth).toHaveBeenCalled(); }); }); test('AutoLogin false scenario', async () => { init({ thoughtSpotHost: 'tshost', customizations: customisations, authType: AuthType.TrustedAuthToken, username: 'tsadmin', getAuthToken: () => Promise.resolve('test_auth_token4'), }); const mockEmbedEventPayload = { type: EmbedEvent.AuthExpire, data: {}, }; const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig); jest.spyOn(baseInstance, 'notifyAuthFailure'); jest.spyOn(baseInstance, 'handleAuth'); searchEmbed.render(); await executeAfterWait(() => { const iframe = getIFrameEl(); postMessageToParent(iframe.contentWindow, mockEmbedEventPayload); }); await executeAfterWait(() => { expect(baseInstance.notifyAuthFailure).toHaveBeenCalledWith( authInstance.AuthFailureType.EXPIRY, ); expect(baseInstance.handleAuth).not.toHaveBeenCalled(); }); }); }); describe('when thoughtSpotHost have value and authPromise return response true/false', () => { beforeAll(() => { init({ thoughtSpotHost, authType: AuthType.None, loginFailedMessage: 'Failed to Login', }); }); const setup = async (isLoggedIn = false) => { jest.spyOn(window, 'addEventListener').mockImplementationOnce( (event, handler, options) => { (handler as EventListener)({ data: { type: 'xyz' }, ports: [3000], source: null, } as any); }, ); const iFrame: any = document.createElement('div'); jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(isLoggedIn); const tsEmbed = new SearchEmbed(getRootEl(), {}); iFrame.contentWindow = null; tsEmbed.on(EmbedEvent.CustomAction, jest.fn()); jest.spyOn(iFrame, 'addEventListener').mockImplementationOnce( (event, handler, options) => { (handler as EventListener)({} as Event); }, ); jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame); await tsEmbed.render(); }; test('mixpanel should call with VISUAL_SDK_RENDER_COMPLETE', async () => { await setup(true); expect(mockMixPanelEvent).toHaveBeenCalledWith(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START); expect(mockMixPanelEvent).toHaveBeenCalledWith( MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE, expect.objectContaining({ elWidth: 0, elHeight: 0, }), ); }); test('Should remove prefetch iframe', async () => { await setup(true); const prefetchIframe = document.querySelectorAll<HTMLIFrameElement>('.prefetchIframe'); expect(prefetchIframe.length).toBe(0); }); test('Should render failure when login fails', async () => { await setup(false); expect(getRootEl().innerHTML).toContain('Failed to Login'); }); }); describe('Trigger infoSuccess event on iframe load', () => { beforeAll(() => { jest.clearAllMocks(); init({ thoughtSpotHost, authType: AuthType.None, loginFailedMessage: 'Failed to Login', }); }); const setup = async (isLoggedIn = false, overrideOrgId: number | undefined = undefined) => { jest.spyOn(window, 'addEventListener').mockImplementationOnce( (event, handler, options) => { (handler as EventListener)({ data: { type: 'xyz' }, ports: [3000], source: null, } as any); }, ); mockProcessTrigger.mockResolvedValueOnce({ session: 'test' }); // resetCachedPreauthInfo(); let mockGetPreauthInfo = null; if (overrideOrgId) { mockGetPreauthInfo =