@thoughtspot/visual-embed-sdk 1.49.3 → 1.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/package.json +1 -1
- package/cjs/src/authToken.spec.js +5 -0
- package/cjs/src/authToken.spec.js.map +1 -1
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js +6 -0
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/app.spec.js +11 -2
- package/cjs/src/embed/app.spec.js.map +1 -1
- package/cjs/src/embed/conversation.d.ts +12 -0
- package/cjs/src/embed/conversation.d.ts.map +1 -1
- package/cjs/src/embed/conversation.js.map +1 -1
- package/cjs/src/embed/conversation.spec.js +30 -0
- package/cjs/src/embed/conversation.spec.js.map +1 -1
- package/cjs/src/embed/embedConfig.spec.d.ts +2 -0
- package/cjs/src/embed/embedConfig.spec.d.ts.map +1 -0
- package/cjs/src/embed/embedConfig.spec.js +74 -0
- package/cjs/src/embed/embedConfig.spec.js.map +1 -0
- package/cjs/src/embed/host-events.spec.d.ts +2 -0
- package/cjs/src/embed/host-events.spec.d.ts.map +1 -0
- package/cjs/src/embed/host-events.spec.js +1219 -0
- package/cjs/src/embed/host-events.spec.js.map +1 -0
- package/cjs/src/mixpanel-service.spec.js +14 -0
- package/cjs/src/mixpanel-service.spec.js.map +1 -1
- package/cjs/src/types.d.ts +93 -6
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +88 -3
- package/cjs/src/types.js.map +1 -1
- package/cjs/src/utils/custom-actions.spec.js +26 -0
- package/cjs/src/utils/custom-actions.spec.js.map +1 -1
- package/cjs/src/utils/processData.spec.js +30 -0
- package/cjs/src/utils/processData.spec.js.map +1 -1
- package/cjs/src/utils/reporting.spec.js +21 -0
- package/cjs/src/utils/reporting.spec.js.map +1 -1
- package/cjs/src/utils/sessionInfoService.spec.d.ts +2 -0
- package/cjs/src/utils/sessionInfoService.spec.d.ts.map +1 -0
- package/cjs/src/utils/sessionInfoService.spec.js +130 -0
- package/cjs/src/utils/sessionInfoService.spec.js.map +1 -0
- package/cjs/src/utils.spec.js +158 -0
- package/cjs/src/utils.spec.js.map +1 -1
- package/dist/{index-DsSQndIB.js → index-B5xoN6Cp.js} +1 -1
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/conversation.d.ts +12 -0
- package/dist/src/embed/conversation.d.ts.map +1 -1
- package/dist/src/embed/embedConfig.spec.d.ts +2 -0
- package/dist/src/embed/embedConfig.spec.d.ts.map +1 -0
- package/dist/src/embed/host-events.spec.d.ts +2 -0
- package/dist/src/embed/host-events.spec.d.ts.map +1 -0
- package/dist/src/types.d.ts +93 -6
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/sessionInfoService.spec.d.ts +2 -0
- package/dist/src/utils/sessionInfoService.spec.d.ts.map +1 -0
- package/dist/tsembed-react.es.js +97 -6
- package/dist/tsembed-react.js +96 -5
- package/dist/tsembed.es.js +97 -6
- package/dist/tsembed.js +96 -5
- package/dist/visual-embed-sdk-react-full.d.ts +105 -6
- package/dist/visual-embed-sdk-react.d.ts +105 -6
- package/dist/visual-embed-sdk.d.ts +105 -6
- package/lib/package.json +1 -1
- package/lib/src/authToken.spec.js +5 -0
- package/lib/src/authToken.spec.js.map +1 -1
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js +6 -0
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/app.spec.js +11 -2
- package/lib/src/embed/app.spec.js.map +1 -1
- package/lib/src/embed/conversation.d.ts +12 -0
- package/lib/src/embed/conversation.d.ts.map +1 -1
- package/lib/src/embed/conversation.js.map +1 -1
- package/lib/src/embed/conversation.spec.js +30 -0
- package/lib/src/embed/conversation.spec.js.map +1 -1
- package/lib/src/embed/embedConfig.spec.d.ts +2 -0
- package/lib/src/embed/embedConfig.spec.d.ts.map +1 -0
- package/lib/src/embed/embedConfig.spec.js +72 -0
- package/lib/src/embed/embedConfig.spec.js.map +1 -0
- package/lib/src/embed/host-events.spec.d.ts +2 -0
- package/lib/src/embed/host-events.spec.d.ts.map +1 -0
- package/lib/src/embed/host-events.spec.js +1216 -0
- package/lib/src/embed/host-events.spec.js.map +1 -0
- package/lib/src/mixpanel-service.spec.js +14 -0
- package/lib/src/mixpanel-service.spec.js.map +1 -1
- package/lib/src/types.d.ts +93 -6
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +88 -3
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/custom-actions.spec.js +26 -0
- package/lib/src/utils/custom-actions.spec.js.map +1 -1
- package/lib/src/utils/processData.spec.js +30 -0
- package/lib/src/utils/processData.spec.js.map +1 -1
- package/lib/src/utils/reporting.spec.js +21 -0
- package/lib/src/utils/reporting.spec.js.map +1 -1
- package/lib/src/utils/sessionInfoService.spec.d.ts +2 -0
- package/lib/src/utils/sessionInfoService.spec.d.ts.map +1 -0
- package/lib/src/utils/sessionInfoService.spec.js +127 -0
- package/lib/src/utils/sessionInfoService.spec.js.map +1 -0
- package/lib/src/utils.spec.js +159 -1
- package/lib/src/utils.spec.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +105 -6
- package/package.json +1 -1
- package/src/authToken.spec.ts +6 -0
- package/src/embed/app.spec.ts +15 -2
- package/src/embed/app.ts +6 -0
- package/src/embed/conversation.spec.ts +32 -0
- package/src/embed/conversation.ts +12 -0
- package/src/embed/embedConfig.spec.ts +79 -0
- package/src/embed/host-events.spec.ts +1635 -0
- package/src/mixpanel-service.spec.ts +15 -0
- package/src/types.ts +96 -6
- package/src/utils/custom-actions.spec.ts +28 -0
- package/src/utils/processData.spec.ts +32 -0
- package/src/utils/reporting.spec.ts +27 -0
- package/src/utils/sessionInfoService.spec.ts +151 -0
- package/src/utils.spec.ts +195 -0
|
@@ -89,4 +89,19 @@ describe('Unit test for mixpanel', () => {
|
|
|
89
89
|
expect(logger.error).toHaveBeenCalled();
|
|
90
90
|
expect(mixpanel.register_once).not.toHaveBeenCalled();
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
test('logs error when mixpanel.init throws (covers catch block)', () => {
|
|
94
|
+
testResetMixpanel();
|
|
95
|
+
jest.spyOn(logger, 'error').mockImplementation(() => {});
|
|
96
|
+
(mixpanel.init as jest.Mock).mockImplementationOnce(() => {
|
|
97
|
+
throw new Error('mixpanel init failed');
|
|
98
|
+
});
|
|
99
|
+
const sessionInfo = {
|
|
100
|
+
mixpanelToken: 'abc123',
|
|
101
|
+
userGUID: '12345',
|
|
102
|
+
isPublicUser: false,
|
|
103
|
+
} as SessionInfo;
|
|
104
|
+
initMixpanel(sessionInfo);
|
|
105
|
+
expect(logger.error).toHaveBeenCalledWith('Error initializing mixpanel', expect.any(Error));
|
|
106
|
+
});
|
|
92
107
|
});
|
package/src/types.ts
CHANGED
|
@@ -963,7 +963,8 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
963
963
|
* The list of actions to completely remove from the embedded view.
|
|
964
964
|
* Hidden actions are not visible to the user at all (fully removed from the UI).
|
|
965
965
|
* Use this when you want to remove an action entirely.
|
|
966
|
-
* To keep an action visible but non-interactive (grayed out), use {@link
|
|
966
|
+
* To keep an action visible but non-interactive (grayed out), use {@link
|
|
967
|
+
* disabledActions} instead.
|
|
967
968
|
*
|
|
968
969
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
969
970
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -983,7 +984,8 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
983
984
|
* (...), and the contextual menu. These will be only actions that
|
|
984
985
|
* are visible to the user.
|
|
985
986
|
* Use this as an allowlist — only the actions listed here will be shown.
|
|
986
|
-
* All other actions will be hidden. Use either this or {@link hiddenActions}, not
|
|
987
|
+
* All other actions will be hidden. Use either this or {@link hiddenActions}, not
|
|
988
|
+
* both.
|
|
987
989
|
*
|
|
988
990
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
989
991
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -3760,10 +3762,11 @@ export enum EmbedEvent {
|
|
|
3760
3762
|
EmbedPageContextChanged = 'EmbedPageContextChanged',
|
|
3761
3763
|
|
|
3762
3764
|
/**
|
|
3763
|
-
* Represents a special embed event that is triggered whenever any host event is
|
|
3765
|
+
* Represents a special embed event that is triggered whenever any host event is
|
|
3766
|
+
* subscribed.
|
|
3764
3767
|
*
|
|
3765
|
-
* You can listen to this event when you need to dispatch a host event during load or
|
|
3766
|
-
* particularly in situations where timing issues may occur.
|
|
3768
|
+
* You can listen to this event when you need to dispatch a host event during load or
|
|
3769
|
+
* render, particularly in situations where timing issues may occur.
|
|
3767
3770
|
*
|
|
3768
3771
|
* @example
|
|
3769
3772
|
* ```js
|
|
@@ -6016,6 +6019,27 @@ export enum HostEvent {
|
|
|
6016
6019
|
* ```
|
|
6017
6020
|
*/
|
|
6018
6021
|
InitSpotterVizConversation = 'InitSpotterVizConversation',
|
|
6022
|
+
|
|
6023
|
+
/**
|
|
6024
|
+
* Opens the SpotterViz panel.
|
|
6025
|
+
* @version SDK: 1.50.0 | ThoughtSpot Cloud: 26.7.0.cl
|
|
6026
|
+
* @example
|
|
6027
|
+
* ```js
|
|
6028
|
+
* liveboardEmbed.trigger(HostEvent.OpenSpotterVizPanel);
|
|
6029
|
+
* ```
|
|
6030
|
+
*/
|
|
6031
|
+
OpenSpotterVizPanel = 'OpenSpotterVizPanel',
|
|
6032
|
+
|
|
6033
|
+
/**
|
|
6034
|
+
* Closes the SpotterViz panel.
|
|
6035
|
+
* @version SDK: 1.50.0 | ThoughtSpot Cloud: 26.7.0.cl
|
|
6036
|
+
* @example
|
|
6037
|
+
* ```js
|
|
6038
|
+
* liveboardEmbed.trigger(HostEvent.CloseSpotterVizPanel);
|
|
6039
|
+
* ```
|
|
6040
|
+
*/
|
|
6041
|
+
CloseSpotterVizPanel = 'CloseSpotterVizPanel',
|
|
6042
|
+
|
|
6019
6043
|
/**
|
|
6020
6044
|
* Clears browser cache and fetches new data for liveboard ChartViz Containers.
|
|
6021
6045
|
* Requires `enableLiveboardDataCache` to be enabled.
|
|
@@ -7972,7 +7996,73 @@ export enum Action {
|
|
|
7972
7996
|
* ```
|
|
7973
7997
|
* @version SDK: 1.49.0 | ThoughtSpot Cloud: 26.6.0.cl
|
|
7974
7998
|
*/
|
|
7975
|
-
RefreshLiveboardBrowserCache = 'refreshLiveboardBrowserCache',
|
|
7999
|
+
RefreshLiveboardBrowserCache = 'refreshLiveboardBrowserCache',
|
|
8000
|
+
/**
|
|
8001
|
+
* Controls visibility and disable state of the share action
|
|
8002
|
+
* in the Spotter Analyst interface.
|
|
8003
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8004
|
+
* @example
|
|
8005
|
+
* ```js
|
|
8006
|
+
* hiddenActions: [Action.SpotterAnalystShare]
|
|
8007
|
+
* disabledActions: [Action.SpotterAnalystShare]
|
|
8008
|
+
* ```
|
|
8009
|
+
*/
|
|
8010
|
+
SpotterAnalystShare = 'spotterAnalystShare',
|
|
8011
|
+
/**
|
|
8012
|
+
* Controls visibility and disable state of the edit action
|
|
8013
|
+
* in the Spotter Analyst interface.
|
|
8014
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8015
|
+
* @example
|
|
8016
|
+
* ```js
|
|
8017
|
+
* hiddenActions: [Action.SpotterAnalystEdit]
|
|
8018
|
+
* disabledActions: [Action.SpotterAnalystEdit]
|
|
8019
|
+
* ```
|
|
8020
|
+
*/
|
|
8021
|
+
SpotterAnalystEdit = 'spotterAnalystEdit',
|
|
8022
|
+
/**
|
|
8023
|
+
* Controls visibility and disable state of the create action
|
|
8024
|
+
* in the Spotter Analyst interface.
|
|
8025
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8026
|
+
* @example
|
|
8027
|
+
* ```js
|
|
8028
|
+
* hiddenActions: [Action.SpotterAnalystCreate]
|
|
8029
|
+
* disabledActions: [Action.SpotterAnalystCreate]
|
|
8030
|
+
* ```
|
|
8031
|
+
*/
|
|
8032
|
+
SpotterAnalystCreate = 'spotterAnalystCreate',
|
|
8033
|
+
/**
|
|
8034
|
+
* Controls visibility and disable state of the delete action
|
|
8035
|
+
* in the Spotter Analyst interface.
|
|
8036
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8037
|
+
* @example
|
|
8038
|
+
* ```js
|
|
8039
|
+
* hiddenActions: [Action.SpotterAnalystDelete]
|
|
8040
|
+
* disabledActions: [Action.SpotterAnalystDelete]
|
|
8041
|
+
* ```
|
|
8042
|
+
*/
|
|
8043
|
+
SpotterAnalystDelete = 'spotterAnalystDelete',
|
|
8044
|
+
/**
|
|
8045
|
+
* Controls visibility and disable state of the make a copy action
|
|
8046
|
+
* in the Spotter Analyst interface.
|
|
8047
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8048
|
+
* @example
|
|
8049
|
+
* ```js
|
|
8050
|
+
* hiddenActions: [Action.SpotterAnalystMakeACopy]
|
|
8051
|
+
* disabledActions: [Action.SpotterAnalystMakeACopy]
|
|
8052
|
+
* ```
|
|
8053
|
+
*/
|
|
8054
|
+
SpotterAnalystMakeACopy = 'spotterAnalystMakeACopy',
|
|
8055
|
+
/**
|
|
8056
|
+
* Controls visibility and disable state of the sidebar
|
|
8057
|
+
* in the Spotter Analyst interface.
|
|
8058
|
+
* @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
|
|
8059
|
+
* @example
|
|
8060
|
+
* ```js
|
|
8061
|
+
* hiddenActions: [Action.SpotterAnalystSidebar]
|
|
8062
|
+
* disabledActions: [Action.SpotterAnalystSidebar]
|
|
8063
|
+
* ```
|
|
8064
|
+
*/
|
|
8065
|
+
SpotterAnalystSidebar = 'spotterAnalystSidebar',
|
|
7976
8066
|
}
|
|
7977
8067
|
export interface AnswerServiceType {
|
|
7978
8068
|
getAnswer?: (offset: number, batchSize: number) => any;
|
|
@@ -429,6 +429,34 @@ describe('getCustomActions function', () => {
|
|
|
429
429
|
});
|
|
430
430
|
});
|
|
431
431
|
|
|
432
|
+
describe('Empty allowed IDs edge cases', () => {
|
|
433
|
+
test('should show "none" as supported metadata IDs when target has no allowed metadata IDs (SPOTTER)', () => {
|
|
434
|
+
const action = {
|
|
435
|
+
id: 'test-id',
|
|
436
|
+
name: 'Test Action',
|
|
437
|
+
target: CustomActionTarget.SPOTTER,
|
|
438
|
+
position: CustomActionsPosition.MENU,
|
|
439
|
+
metadataIds: { liveboardIds: ['lb-1'] },
|
|
440
|
+
} as any;
|
|
441
|
+
const result = getCustomActions([action]);
|
|
442
|
+
// Expect an error containing 'none' for unsupported metadataIds on SPOTTER
|
|
443
|
+
expect(result.errors.some((e) => e.includes('none'))).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('should show "none" as supported data model IDs when target has no allowed data model IDs (LIVEBOARD)', () => {
|
|
447
|
+
const action = {
|
|
448
|
+
id: 'test-id',
|
|
449
|
+
name: 'Test Action',
|
|
450
|
+
target: CustomActionTarget.LIVEBOARD,
|
|
451
|
+
position: CustomActionsPosition.PRIMARY,
|
|
452
|
+
dataModelIds: { modelIds: ['model-1'] },
|
|
453
|
+
} as any;
|
|
454
|
+
const result = getCustomActions([action]);
|
|
455
|
+
// Expect an error containing 'none' for unsupported dataModelIds on LIVEBOARD
|
|
456
|
+
expect(result.errors.some((e) => e.includes('none'))).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
432
460
|
describe('Warnings', () => {
|
|
433
461
|
test('should warn when action name length exceeds 30 characters', () => {
|
|
434
462
|
// Arrange
|
|
@@ -279,6 +279,38 @@ describe('Unit test for process data', () => {
|
|
|
279
279
|
mockHandleExitPresentMode.mockReset();
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
+
test('AuthInit with payload.userGUID fallback when data.userGUID is absent', () => {
|
|
283
|
+
const e = {
|
|
284
|
+
type: EmbedEvent.AuthInit,
|
|
285
|
+
payload: { userGUID: 'payloadGUID' },
|
|
286
|
+
};
|
|
287
|
+
jest.spyOn(base, 'notifyAuthSuccess');
|
|
288
|
+
expect(processDataInstance.processEventData(e.type, e, '', null)).toEqual({
|
|
289
|
+
type: e.type,
|
|
290
|
+
payload: { userGUID: 'payloadGUID' },
|
|
291
|
+
data: { userGUID: 'payloadGUID' },
|
|
292
|
+
});
|
|
293
|
+
expect(base.notifyAuthSuccess).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('process authFailure EmbeddedSSO with UNAUTHENTICATED_FAILURE suppresses login page', () => {
|
|
297
|
+
const e = {
|
|
298
|
+
type: EmbedEvent.AuthFailure,
|
|
299
|
+
data: { type: auth.AuthFailureType.UNAUTHENTICATED_FAILURE },
|
|
300
|
+
};
|
|
301
|
+
jest.spyOn(base, 'notifyAuthFailure');
|
|
302
|
+
jest.spyOn(embedConfigInstance, 'getEmbedConfig').mockReturnValue({
|
|
303
|
+
loginFailedMessage: 'Hello',
|
|
304
|
+
authType: AuthType.EmbeddedSSO,
|
|
305
|
+
disableLoginFailurePage: false,
|
|
306
|
+
} as any);
|
|
307
|
+
const el: any = {};
|
|
308
|
+
processDataInstance.processEventData(e.type, e, '', el);
|
|
309
|
+
// isEmbeddedSSOInfoFailure=true so neither branch fires, innerHTML stays unset
|
|
310
|
+
expect(el.innerHTML).toBeUndefined();
|
|
311
|
+
expect(base.notifyAuthFailure).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
282
314
|
test('should handle ClearInfoCache', () => {
|
|
283
315
|
const mockResetCachedPreauthInfo = jest.spyOn(sessionInfoService, 'resetCachedPreauthInfo').mockImplementation(() => {});
|
|
284
316
|
const mockResetCachedSessionInfo = jest.spyOn(sessionInfoService, 'resetCachedSessionInfo').mockImplementation(() => {});
|
|
@@ -88,4 +88,31 @@ describe('ReportingObserver', () => {
|
|
|
88
88
|
|
|
89
89
|
(window as any).ReportingObserver = currentObserver;
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
test('should suppress alert but still log error when suppressErrorAlerts is true', () => {
|
|
93
|
+
let callBackPassed: any;
|
|
94
|
+
const NewMockRO = jest.fn().mockImplementation((callback: any) => {
|
|
95
|
+
callBackPassed = callback;
|
|
96
|
+
return ({ observe: jest.fn(), disconnect: jest.fn(), takeRecords: jest.fn() });
|
|
97
|
+
});
|
|
98
|
+
const currentObserver = (window as any).ReportingObserver;
|
|
99
|
+
(window as any).ReportingObserver = NewMockRO;
|
|
100
|
+
|
|
101
|
+
setEmbedConfig({
|
|
102
|
+
thoughtSpotHost: 'testHost',
|
|
103
|
+
authType: AuthType.None,
|
|
104
|
+
suppressErrorAlerts: true,
|
|
105
|
+
} as any);
|
|
106
|
+
|
|
107
|
+
const mockAlert = jest.fn();
|
|
108
|
+
window.alert = mockAlert;
|
|
109
|
+
|
|
110
|
+
registerReportingObserver(true);
|
|
111
|
+
callBackPassed([{ type: 'csp-violation', url: 'testHost', body: { effectiveDirective: 'frame-ancestors' } }]);
|
|
112
|
+
|
|
113
|
+
expect(mockAlert).not.toHaveBeenCalled();
|
|
114
|
+
expect(logger.error).toHaveBeenCalledWith(ERROR_MESSAGE.CSP_FRAME_HOST_VIOLATION_LOG_MESSAGE);
|
|
115
|
+
|
|
116
|
+
(window as any).ReportingObserver = currentObserver;
|
|
117
|
+
});
|
|
91
118
|
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as authServiceModule from './authService';
|
|
2
|
+
import * as embedConfigModule from '../embed/embedConfig';
|
|
3
|
+
import {
|
|
4
|
+
formatPreauthInfo,
|
|
5
|
+
getPreauthInfo,
|
|
6
|
+
getSessionInfo,
|
|
7
|
+
resetCachedPreauthInfo,
|
|
8
|
+
resetCachedSessionInfo,
|
|
9
|
+
getCachedSessionInfo,
|
|
10
|
+
getSessionDetails,
|
|
11
|
+
} from './sessionInfoService';
|
|
12
|
+
|
|
13
|
+
describe('sessionInfoService', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
resetCachedPreauthInfo();
|
|
16
|
+
resetCachedSessionInfo();
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('formatPreauthInfo', () => {
|
|
21
|
+
it('parses headers and JSON body from a response', async () => {
|
|
22
|
+
const headers = new Headers({ 'content-type': 'application/json' });
|
|
23
|
+
const mockResp = {
|
|
24
|
+
headers,
|
|
25
|
+
json: jest.fn().mockResolvedValue({ info: { userGUID: 'u1' } }),
|
|
26
|
+
};
|
|
27
|
+
const result = await formatPreauthInfo(mockResp);
|
|
28
|
+
expect(result.info.userGUID).toBe('u1');
|
|
29
|
+
expect(result.status).toBe(200);
|
|
30
|
+
expect(result.headers['content-type']).toBe('application/json');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null when response.json() throws (covers catch block)', async () => {
|
|
34
|
+
const mockResp = {
|
|
35
|
+
headers: null as any,
|
|
36
|
+
json: jest.fn().mockRejectedValue(new Error('parse error')),
|
|
37
|
+
};
|
|
38
|
+
const result = await formatPreauthInfo(mockResp);
|
|
39
|
+
expect(result).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles response where headers is null (covers headers?.forEach branch)', async () => {
|
|
43
|
+
const mockResp = {
|
|
44
|
+
headers: null as any,
|
|
45
|
+
json: jest.fn().mockResolvedValue({ info: {} }),
|
|
46
|
+
};
|
|
47
|
+
const result = await formatPreauthInfo(mockResp);
|
|
48
|
+
expect(result.headers).toEqual({});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getPreauthInfo', () => {
|
|
53
|
+
it('fetches and caches preauthInfo on first call', async () => {
|
|
54
|
+
const mockResp = {
|
|
55
|
+
headers: new Headers({ 'x-caller': 'test' }),
|
|
56
|
+
json: jest.fn().mockResolvedValue({ info: { userGUID: 'u1' } }),
|
|
57
|
+
};
|
|
58
|
+
jest.spyOn(embedConfigModule, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: 'https://ts' } as any);
|
|
59
|
+
jest.spyOn(authServiceModule, 'fetchPreauthInfoService').mockResolvedValue(mockResp as any);
|
|
60
|
+
|
|
61
|
+
const result = await getPreauthInfo();
|
|
62
|
+
expect(result.info.userGUID).toBe('u1');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns cached preauthInfo on subsequent calls (covers allowCache=true && preauthInfo cached branch)', async () => {
|
|
66
|
+
const mockResp = {
|
|
67
|
+
headers: new Headers(),
|
|
68
|
+
json: jest.fn().mockResolvedValue({ info: { userGUID: 'cached-user' } }),
|
|
69
|
+
};
|
|
70
|
+
jest.spyOn(embedConfigModule, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: 'https://ts' } as any);
|
|
71
|
+
const fetchSpy = jest.spyOn(authServiceModule, 'fetchPreauthInfoService').mockResolvedValue(mockResp as any);
|
|
72
|
+
|
|
73
|
+
await getPreauthInfo(); // first call — fetches and caches
|
|
74
|
+
await getPreauthInfo(); // second call — should use cache
|
|
75
|
+
|
|
76
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns null when fetch throws (covers catch block)', async () => {
|
|
80
|
+
jest.spyOn(embedConfigModule, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: 'https://ts' } as any);
|
|
81
|
+
jest.spyOn(authServiceModule, 'fetchPreauthInfoService').mockRejectedValue(new Error('network error'));
|
|
82
|
+
|
|
83
|
+
const result = await getPreauthInfo();
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('getSessionInfo', () => {
|
|
89
|
+
it('fetches and returns session info', async () => {
|
|
90
|
+
const sessionInfoResp = {
|
|
91
|
+
userGUID: 'u1',
|
|
92
|
+
releaseVersion: '1.0',
|
|
93
|
+
configInfo: {
|
|
94
|
+
mixpanelConfig: { devSdkKey: 'dev', prodSdkKey: 'prod', production: false },
|
|
95
|
+
isPublicUser: false,
|
|
96
|
+
selfClusterId: 'c1',
|
|
97
|
+
selfClusterName: 'cluster1',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
jest.spyOn(embedConfigModule, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: 'https://ts' } as any);
|
|
101
|
+
jest.spyOn(authServiceModule, 'fetchSessionInfoService').mockResolvedValue(sessionInfoResp as any);
|
|
102
|
+
|
|
103
|
+
const result = await getSessionInfo();
|
|
104
|
+
expect(result.userGUID).toBe('u1');
|
|
105
|
+
expect(result.mixpanelToken).toBe('dev');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns cached session info on second call', async () => {
|
|
109
|
+
const sessionInfoResp = {
|
|
110
|
+
userGUID: 'u2',
|
|
111
|
+
releaseVersion: '2.0',
|
|
112
|
+
configInfo: {
|
|
113
|
+
mixpanelConfig: { devSdkKey: 'dev2', prodSdkKey: 'prod2', production: true },
|
|
114
|
+
isPublicUser: false,
|
|
115
|
+
selfClusterId: 'c2',
|
|
116
|
+
selfClusterName: 'cluster2',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
jest.spyOn(embedConfigModule, 'getEmbedConfig').mockReturnValue({ thoughtSpotHost: 'https://ts' } as any);
|
|
120
|
+
const fetchSpy = jest.spyOn(authServiceModule, 'fetchSessionInfoService').mockResolvedValue(sessionInfoResp as any);
|
|
121
|
+
|
|
122
|
+
await getSessionInfo();
|
|
123
|
+
await getSessionInfo();
|
|
124
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('getCachedSessionInfo', () => {
|
|
129
|
+
it('returns null before any fetch', () => {
|
|
130
|
+
expect(getCachedSessionInfo()).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('getSessionDetails', () => {
|
|
135
|
+
it('uses prodSdkKey when production flag is true', () => {
|
|
136
|
+
const resp = {
|
|
137
|
+
userGUID: 'u3',
|
|
138
|
+
releaseVersion: '3.0',
|
|
139
|
+
configInfo: {
|
|
140
|
+
mixpanelConfig: { devSdkKey: 'dev', prodSdkKey: 'prod', production: true },
|
|
141
|
+
isPublicUser: true,
|
|
142
|
+
selfClusterId: 'c3',
|
|
143
|
+
selfClusterName: 'cluster3',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const result = getSessionDetails(resp);
|
|
147
|
+
expect(result.mixpanelToken).toBe('prod');
|
|
148
|
+
expect(result.isPublicUser).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
package/src/utils.spec.ts
CHANGED
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
validateHttpUrl,
|
|
28
28
|
setParamIfDefined,
|
|
29
29
|
querySelectorAcrossShadowRoot,
|
|
30
|
+
getSSOMarker,
|
|
31
|
+
deepMerge,
|
|
32
|
+
getHostEventsConfig,
|
|
33
|
+
isWindowUndefined,
|
|
34
|
+
getOffsetTop,
|
|
35
|
+
getDOMNode,
|
|
36
|
+
getOperationNameFromQuery,
|
|
30
37
|
} from './utils';
|
|
31
38
|
import { RuntimeFilterOp } from './types';
|
|
32
39
|
import { logger } from './utils/logger';
|
|
@@ -388,6 +395,18 @@ describe('Fullscreen Utility Functions', () => {
|
|
|
388
395
|
expect(logger.error).toHaveBeenCalledWith('Fullscreen API is not supported by this browser.');
|
|
389
396
|
});
|
|
390
397
|
|
|
398
|
+
it('should catch error and log warning when requestFullscreen throws (covers line 506)', async () => {
|
|
399
|
+
// Covers line 506: catch block inside fullscreen method attempt
|
|
400
|
+
(mockIframe.requestFullscreen as jest.Mock).mockRejectedValue(new Error('permission denied'));
|
|
401
|
+
|
|
402
|
+
await handlePresentEvent(mockIframe);
|
|
403
|
+
|
|
404
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
405
|
+
expect.stringContaining('Failed to enter fullscreen'),
|
|
406
|
+
expect.any(Error),
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
391
410
|
it('should not attempt fullscreen when already in fullscreen mode', () => {
|
|
392
411
|
Object.defineProperty(document, 'fullscreenElement', {
|
|
393
412
|
writable: true,
|
|
@@ -1009,6 +1028,24 @@ describe('isValidCssMargin', () => {
|
|
|
1009
1028
|
expect(isValidCssMargin('invalid')).toBe(false);
|
|
1010
1029
|
expect(isValidCssMargin('10')).toBe(false); // missing unit
|
|
1011
1030
|
});
|
|
1031
|
+
|
|
1032
|
+
it('should return false and log error when value is not a string (non-string type)', () => {
|
|
1033
|
+
// Covers line 147-148: typeof value !== 'string' branch
|
|
1034
|
+
expect(isValidCssMargin(42 as any)).toBe(false);
|
|
1035
|
+
expect(logger.error).toHaveBeenCalledWith('Please provide a valid lazyLoadingMargin value (e.g., "10px")');
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it('should return false when more than 4 space-separated parts are given', () => {
|
|
1039
|
+
// Covers line 157-158: parts.length > 4 branch
|
|
1040
|
+
expect(isValidCssMargin('10px 20px 30px 40px 50px')).toBe(false);
|
|
1041
|
+
expect(logger.error).toHaveBeenCalledWith('Please provide a valid lazyLoadingMargin value (e.g., "10px")');
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('should return true for valid multi-part margin (up to 4 parts)', () => {
|
|
1045
|
+
expect(isValidCssMargin('10px 20px')).toBe(true);
|
|
1046
|
+
expect(isValidCssMargin('10px 20px 30px 40px')).toBe(true);
|
|
1047
|
+
expect(isValidCssMargin('auto')).toBe(true);
|
|
1048
|
+
});
|
|
1012
1049
|
});
|
|
1013
1050
|
|
|
1014
1051
|
describe('getValueFromWindow and storeValueInWindow', () => {
|
|
@@ -1140,3 +1177,161 @@ describe('getValueFromWindow and storeValueInWindow', () => {
|
|
|
1140
1177
|
});
|
|
1141
1178
|
});
|
|
1142
1179
|
});
|
|
1180
|
+
|
|
1181
|
+
// ---------------------------------------------------------------------------
|
|
1182
|
+
// getSSOMarker
|
|
1183
|
+
// ---------------------------------------------------------------------------
|
|
1184
|
+
describe('getSSOMarker', () => {
|
|
1185
|
+
test('returns tsSSOMarker query string with encoded markerId', () => {
|
|
1186
|
+
expect(getSSOMarker('abc123')).toBe('tsSSOMarker=abc123');
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test('URL-encodes special characters in markerId', () => {
|
|
1190
|
+
expect(getSSOMarker('hello world')).toBe('tsSSOMarker=hello%20world');
|
|
1191
|
+
expect(getSSOMarker('foo=bar&baz')).toBe('tsSSOMarker=foo%3Dbar%26baz');
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test('handles empty string markerId', () => {
|
|
1195
|
+
expect(getSSOMarker('')).toBe('tsSSOMarker=');
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// ---------------------------------------------------------------------------
|
|
1200
|
+
// deepMerge
|
|
1201
|
+
// ---------------------------------------------------------------------------
|
|
1202
|
+
describe('deepMerge', () => {
|
|
1203
|
+
test('merges two flat objects', () => {
|
|
1204
|
+
const result = deepMerge({ a: 1 }, { b: 2 });
|
|
1205
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test('source overrides target for same keys', () => {
|
|
1209
|
+
const result = deepMerge({ a: 1, b: 2 }, { b: 99 });
|
|
1210
|
+
expect(result).toEqual({ a: 1, b: 99 });
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
test('deep merges nested objects', () => {
|
|
1214
|
+
const result = deepMerge({ a: { x: 1, y: 2 } }, { a: { y: 99, z: 3 } });
|
|
1215
|
+
expect(result).toEqual({ a: { x: 1, y: 99, z: 3 } });
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test('handles empty source', () => {
|
|
1219
|
+
const result = deepMerge({ a: 1 }, {});
|
|
1220
|
+
expect(result).toEqual({ a: 1 });
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test('handles empty target', () => {
|
|
1224
|
+
const result = deepMerge({}, { b: 2 });
|
|
1225
|
+
expect(result).toEqual({ b: 2 });
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// ---------------------------------------------------------------------------
|
|
1230
|
+
// getHostEventsConfig
|
|
1231
|
+
// ---------------------------------------------------------------------------
|
|
1232
|
+
describe('getHostEventsConfig', () => {
|
|
1233
|
+
test('returns shouldBypassPayloadValidation and useHostEventsV2 from viewConfig', () => {
|
|
1234
|
+
const viewConfig: any = { shouldBypassPayloadValidation: true, useHostEventsV2: false };
|
|
1235
|
+
expect(getHostEventsConfig(viewConfig)).toEqual({
|
|
1236
|
+
shouldBypassPayloadValidation: true,
|
|
1237
|
+
useHostEventsV2: false,
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
test('returns undefined for missing keys', () => {
|
|
1242
|
+
const result = getHostEventsConfig({} as any);
|
|
1243
|
+
expect(result.shouldBypassPayloadValidation).toBeUndefined();
|
|
1244
|
+
expect(result.useHostEventsV2).toBeUndefined();
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
test('ignores unrelated config properties', () => {
|
|
1248
|
+
const viewConfig: any = {
|
|
1249
|
+
shouldBypassPayloadValidation: false,
|
|
1250
|
+
useHostEventsV2: true,
|
|
1251
|
+
liveboardId: 'lb-1',
|
|
1252
|
+
};
|
|
1253
|
+
expect(getHostEventsConfig(viewConfig)).toEqual({
|
|
1254
|
+
shouldBypassPayloadValidation: false,
|
|
1255
|
+
useHostEventsV2: true,
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// ---------------------------------------------------------------------------
|
|
1261
|
+
// isWindowUndefined
|
|
1262
|
+
// ---------------------------------------------------------------------------
|
|
1263
|
+
describe('isWindowUndefined', () => {
|
|
1264
|
+
test('returns false when window is defined (browser/jsdom env)', () => {
|
|
1265
|
+
// In jest/jsdom, window is always defined
|
|
1266
|
+
expect(isWindowUndefined()).toBe(false);
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
// getOffsetTop
|
|
1272
|
+
// ---------------------------------------------------------------------------
|
|
1273
|
+
describe('getOffsetTop', () => {
|
|
1274
|
+
test('returns rect.top + window.scrollY', () => {
|
|
1275
|
+
const mockElement = {
|
|
1276
|
+
getBoundingClientRect: () => ({ top: 50 }),
|
|
1277
|
+
};
|
|
1278
|
+
Object.defineProperty(window, 'scrollY', { value: 100, configurable: true });
|
|
1279
|
+
expect(getOffsetTop(mockElement)).toBe(150);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test('returns rect.top when scrollY is 0', () => {
|
|
1283
|
+
const mockElement = {
|
|
1284
|
+
getBoundingClientRect: () => ({ top: 30 }),
|
|
1285
|
+
};
|
|
1286
|
+
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
|
|
1287
|
+
expect(getOffsetTop(mockElement)).toBe(30);
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// ---------------------------------------------------------------------------
|
|
1292
|
+
// getDOMNode
|
|
1293
|
+
// ---------------------------------------------------------------------------
|
|
1294
|
+
describe('getDOMNode', () => {
|
|
1295
|
+
test('returns the element from document.querySelector when passed a string selector', () => {
|
|
1296
|
+
const div = document.createElement('div');
|
|
1297
|
+
div.id = 'test-dom-node';
|
|
1298
|
+
document.body.appendChild(div);
|
|
1299
|
+
expect(getDOMNode('#test-dom-node')).toBe(div);
|
|
1300
|
+
document.body.removeChild(div);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
test('returns the element directly when passed an HTMLElement', () => {
|
|
1304
|
+
const div = document.createElement('div');
|
|
1305
|
+
expect(getDOMNode(div)).toBe(div);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test('returns null for a selector that matches nothing', () => {
|
|
1309
|
+
expect(getDOMNode('#nonexistent-element-xyz')).toBeNull();
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
// ---------------------------------------------------------------------------
|
|
1314
|
+
// getOperationNameFromQuery
|
|
1315
|
+
// ---------------------------------------------------------------------------
|
|
1316
|
+
describe('getOperationNameFromQuery', () => {
|
|
1317
|
+
test('extracts operation name from a query string', () => {
|
|
1318
|
+
expect(getOperationNameFromQuery('query GetUser { user { id } }')).toBe('GetUser');
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test('extracts operation name from a mutation string', () => {
|
|
1322
|
+
expect(getOperationNameFromQuery('mutation CreateUser { createUser { id } }')).toBe('CreateUser');
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
test('returns undefined when no operation name is present', () => {
|
|
1326
|
+
expect(getOperationNameFromQuery('{ user { id } }')).toBeUndefined();
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test('handles multiline query strings', () => {
|
|
1330
|
+
const query = `
|
|
1331
|
+
query FetchData {
|
|
1332
|
+
data { id }
|
|
1333
|
+
}
|
|
1334
|
+
`;
|
|
1335
|
+
expect(getOperationNameFromQuery(query)).toBe('FetchData');
|
|
1336
|
+
});
|
|
1337
|
+
});
|