@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
1,280 lines (1,180 loc) • 170 kB
text/typescript
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¶mVal1=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 =