• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / visual-embed-sdk / #3186

10 Jan 2026 06:28AM UTC coverage: 94.608% (+0.3%) from 94.313%
#3186

Pull #342

ruchI9897
exposed contexttype
Pull Request #342: Host events2

1395 of 1556 branches covered (89.65%)

Branch coverage included in aggregate %.

26 of 40 new or added lines in 12 files covered. (65.0%)

43 existing lines in 7 files now uncovered.

3272 of 3377 relevant lines covered (96.89%)

114.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.08
/src/embed/ts-embed.ts
1
/**
2
 * Copyright (c) 2022
3
 *
4
 * Base classes
5
 * @summary Base classes
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8

9
import isEqual from 'lodash/isEqual';
14✔
10
import isEmpty from 'lodash/isEmpty';
14✔
11
import isObject from 'lodash/isObject';
14✔
12
import {
14✔
13
    TriggerPayload,
14
    TriggerResponse,
15
    UIPassthroughArrayResponse,
16
    UIPassthroughEvent,
17
    UIPassthroughRequest,
18
} from './hostEventClient/contracts';
19
import { logger } from '../utils/logger';
14✔
20
import { getAuthenticationToken } from '../authToken';
14✔
21
import { AnswerService } from '../utils/graphql/answerService/answerService';
14✔
22
import {
14✔
23
    getEncodedQueryParamsString,
24
    getCssDimension,
25
    getOffsetTop,
26
    embedEventStatus,
27
    setAttributes,
28
    getCustomisations,
29
    getRuntimeFilters,
30
    getDOMNode,
31
    getFilterQuery,
32
    getQueryParamString,
33
    getRuntimeParameters,
34
    setStyleProperties,
35
    removeStyleProperties,
36
    isUndefined,
37
} from '../utils';
38
import { getCustomActions } from '../utils/custom-actions';
14✔
39
import {
14✔
40
    getThoughtSpotHost,
41
    URL_MAX_LENGTH,
42
    DEFAULT_EMBED_WIDTH,
43
    DEFAULT_EMBED_HEIGHT,
44
    getV2BasePath,
45
} from '../config';
46
import {
14✔
47
    AuthType,
48
    DOMSelector,
49
    HostEvent,
50
    EmbedEvent,
51
    MessageCallback,
52
    Action,
53
    Param,
54
    EmbedConfig,
55
    MessageOptions,
56
    MessageCallbackObj,
57
    ContextMenuTriggerOptions,
58
    DefaultAppInitData,
59
    AllEmbedViewConfig as ViewConfig,
60
    EmbedErrorDetailsEvent,
61
    ErrorDetailsTypes,
62
    EmbedErrorCodes,
63
    ContextType,
64
} from '../types';
65
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
14✔
66
import { processEventData, processAuthFailure } from '../utils/processData';
14✔
67
import pkgInfo from '../../package.json';
14✔
68
import {
14✔
69
    getAuthPromise, renderInQueue, handleAuth, notifyAuthFailure,
70
    getInitPromise,
71
    getIsInitCalled,
72
} from './base';
73
import { AuthFailureType } from '../auth';
14✔
74
import { getEmbedConfig } from './embedConfig';
14✔
75
import { ERROR_MESSAGE } from '../errors';
14✔
76
import { getPreauthInfo } from '../utils/sessionInfoService';
14✔
77
import { HostEventClient } from './hostEventClient/host-event-client';
14✔
78
import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept';
14✔
79
import { getHostEventsConfig } from '../utils';
14✔
80

81
const { version } = pkgInfo;
14✔
82

83
/**
84
 * Global prefix for all Thoughtspot postHash Params.
85
 */
86
export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
14✔
87
const TS_EMBED_ID = '_thoughtspot-embed';
14✔
88

89
/**
90
 * The event id map from v2 event names to v1 event id
91
 * v1 events are the classic embed events implemented in Blink v1
92
 * We cannot rename v1 event types to maintain backward compatibility
93
 * @internal
94
 */
95
const V1EventMap: Record<string, any> = {};
14✔
96

97
/**
98
 * Base class for embedding v2 experience
99
 * Note: the v2 version of ThoughtSpot Blink is built on the new stack:
100
 * React+GraphQL
101
 */
102
export class TsEmbed {
14✔
103
    /**
104
     * The DOM node which was inserted by the SDK to either
105
     * render the iframe or display an error message.
106
     * This is useful for removing the DOM node when the
107
     * embed instance is destroyed.
108
     */
109
    protected insertedDomEl: Node;
110

111
    /**
112
     * The DOM node where the ThoughtSpot app is to be embedded.
113
     */
114
    protected el: HTMLElement;
115

116
    /**
117
     * The key to store the embed instance in the DOM node
118
     */
119
    protected embedNodeKey = '__tsEmbed';
495✔
120

121
    protected isAppInitialized = false;
495✔
122

123
    /**
124
     * A reference to the iframe within which the ThoughtSpot app
125
     * will be rendered.
126
     */
127
    protected iFrame: HTMLIFrameElement;
128

129
    /**
130
     * Setter for the iframe element
131
     * @param {HTMLIFrameElement} iFrame HTMLIFrameElement
132
     */
133
    protected setIframeElement(iFrame: HTMLIFrameElement): void {
134
        this.iFrame = iFrame;
463✔
135
        this.hostEventClient.setIframeElement(iFrame);
463✔
136
    }
137

138
    protected viewConfig: ViewConfig & { visibleTabs?: string[], hiddenTabs?: string[], showAlerts?: boolean };
139

140
    protected embedConfig: EmbedConfig;
141

142
    /**
143
     * The ThoughtSpot hostname or IP address
144
     */
145
    protected thoughtSpotHost: string;
146

147
    /*
148
     * This is the base to access ThoughtSpot V2.
149
     */
150
    protected thoughtSpotV2Base: string;
151

152
    /**
153
     * A map of event handlers for particular message types triggered
154
     * by the embedded app; multiple event handlers can be registered
155
     * against a particular message type.
156
     */
157
    private eventHandlerMap: Map<string, MessageCallbackObj[]>;
158

159
    /**
160
     * A flag that is set to true post render.
161
     */
162
    protected isRendered: boolean;
163

164
    /**
165
     * A flag to mark if an error has occurred.
166
     */
167
    private isError: boolean;
168

169
    /**
170
     * A flag that is set to true post preRender.
171
     */
172
    private isPreRendered: boolean;
173

174
    /**
175
     * Should we encode URL Query Params using base64 encoding which thoughtspot
176
     * will generate for embedding. This provides additional security to
177
     * thoughtspot clusters against Cross site scripting attacks.
178
     * @default false
179
     */
180
    private shouldEncodeUrlQueryParams = false;
495✔
181

182
    private defaultHiddenActions = [Action.ReportError];
495✔
183

184
    private resizeObserver: ResizeObserver;
185

186
    protected hostEventClient: HostEventClient;
187

188
    protected isReadyForRenderPromise;
189

190
    /**
191
     * Handler for fullscreen change events
192
     */
193
    private fullscreenChangeHandler: (() => void) | null = null;
495✔
194

195
    constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
196
        this.el = getDOMNode(domSelector);
495✔
197
        this.eventHandlerMap = new Map();
495✔
198
        this.isError = false;
495✔
199
        this.viewConfig = {
495✔
200
            excludeRuntimeFiltersfromURL: false,
201
            excludeRuntimeParametersfromURL: false,
202
            ...viewConfig,
203
        };
204
        this.registerAppInit();
495✔
205
        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
495✔
206
            ...viewConfig,
207
        });
208
        const embedConfig = getEmbedConfig();
495✔
209
        this.embedConfig = embedConfig;
495✔
210

211
        this.hostEventClient = new HostEventClient(this.iFrame);
495✔
212
        this.isReadyForRenderPromise = getInitPromise().then(async () => {
495✔
213
            if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
495✔
214
                this.embedConfig.authTriggerContainer = domSelector;
89✔
215
            }
216
            this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
495✔
217
            this.thoughtSpotV2Base = getV2BasePath(embedConfig);
495✔
218
            this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
495✔
219
        });
220
    }
221

222
    /**
223
     * Throws error encountered during initialization.
224
     */
225
    private throwInitError() {
226
        this.handleError({
1✔
227
            errorType: ErrorDetailsTypes.VALIDATION_ERROR,
228
            message: ERROR_MESSAGE.INIT_SDK_REQUIRED,
229
            code: EmbedErrorCodes.INIT_ERROR,
230
            error : ERROR_MESSAGE.INIT_SDK_REQUIRED,
231
        });
232
    }
233

234
    /**
235
     * Handles errors within the SDK
236
     * @param error The error message or object
237
     * @param errorDetails The error details
238
     */
239
    protected handleError(errorDetails: EmbedErrorDetailsEvent) {
240
        this.isError = true;
16✔
241
        this.executeCallbacks(EmbedEvent.Error, errorDetails);
16✔
242
        // Log error
243
        logger.error(errorDetails);
16✔
244
    }
245

246
    /**
247
     * Extracts the type field from the event payload
248
     * @param event The window message event
249
     */
250
    private getEventType(event: MessageEvent) {
251

252
        return event.data?.type || event.data?.__type;
3,155!
253
    }
254

255
    /**
256
     * Extracts the port field from the event payload
257
     * @param event  The window message event
258
     * @returns
259
     */
260
    private getEventPort(event: MessageEvent) {
261
        if (event.ports.length && event.ports[0]) {
3,155✔
262
            return event.ports[0];
2,402✔
263
        }
264
        return null;
753✔
265
    }
266

267
    /**
268
     * Checks if preauth cache is enabled
269
     * from the view config and embed config
270
     * @returns boolean
271
     */
272
    private isPreAuthCacheEnabled() {
273
        // Disable preauth cache when:
274
        // 1. overrideOrgId is present since:
275
        //    - cached auth info would be for wrong org
276
        //    - info call response changes for each different overrideOrgId
277
        // 2. disablePreauthCache is explicitly set to true
278
        // 3. FullAppEmbed has primary navbar visible since:
279
        //    - primary navbar requires fresh auth state for navigation
280
        //    - cached auth may not reflect current user permissions
281
        const isDisabled = (
282
            this.viewConfig.overrideOrgId !== undefined
465✔
283
            || this.embedConfig.disablePreauthCache === true
284
            || this.isFullAppEmbedWithVisiblePrimaryNavbar()
285
        );
286
        return !isDisabled;
465✔
287
    }
288

289
    /**
290
     * Checks if current embed is FullAppEmbed with visible primary navbar
291
     * @returns boolean
292
     */
293
    private isFullAppEmbedWithVisiblePrimaryNavbar(): boolean {
294
        const appViewConfig = this.viewConfig as any;
463✔
295

296
        // Check if this is a FullAppEmbed (AppEmbed)
297
        // showPrimaryNavbar defaults to true if not explicitly set to false
298
        return (
463✔
299
            appViewConfig.embedComponentType === 'AppEmbed'
613✔
300
            && appViewConfig.showPrimaryNavbar === true
301
        );
302
    }
303

304
    /**
305
     * fix for ts7.sep.cl
306
     * will be removed for ts7.oct.cl
307
     * @param event
308
     * @param eventType
309
     * @hidden
310
     */
311
    private formatEventData(event: MessageEvent, eventType: string) {
312
        const eventData = {
3,155✔
313
            ...event.data,
314
            type: eventType,
315
        };
316
        if (!eventData.data) {
3,155✔
317
            eventData.data = event.data.payload;
529✔
318
        }
319
        return eventData;
3,155✔
320
    }
321

322
    private subscribedListeners: Record<string, any> = {};
495✔
323

324
    /**
325
     * Subscribe to network events (online/offline) that should
326
     * work regardless of auth status
327
     */
328
    private subscribeToNetworkEvents() {
329
        this.unsubscribeToNetworkEvents();
453✔
330

331
        const onlineEventListener = (e: Event) => {
453✔
332
            this.trigger(HostEvent.Reload);
265✔
333
        };
334
        window.addEventListener('online', onlineEventListener);
453✔
335

336
        const offlineEventListener = (e: Event) => {
453✔
UNCOV
337
            const errorDetails = {
×
338
                errorType: ErrorDetailsTypes.NETWORK,
339
                message: ERROR_MESSAGE.OFFLINE_WARNING,
340
                code: EmbedErrorCodes.NETWORK_ERROR,
341
                offlineWarning : ERROR_MESSAGE.OFFLINE_WARNING,
342
            };
UNCOV
343
            this.executeCallbacks(EmbedEvent.Error, errorDetails);
×
344
            logger.warn(errorDetails);
×
345
        };
346
        window.addEventListener('offline', offlineEventListener);
453✔
347

348
        this.subscribedListeners.online = onlineEventListener;
453✔
349
        this.subscribedListeners.offline = offlineEventListener;
453✔
350
    }
351

352
    private handleApiInterceptEvent({ eventData, eventPort }: { eventData: any, eventPort: MessagePort | void }) {
353
        const executeEvent = (_eventType: EmbedEvent, data: any) => {
10✔
354
            this.executeCallbacks(_eventType, data, eventPort);
3✔
355
        }
356
        const getUnsavedAnswerTml = async (props: { sessionId?: string, vizId?: string }) => {
10✔
357
            const response = await this.triggerUIPassThrough(UIPassthroughEvent.GetUnsavedAnswerTML, props);
2✔
358
            return response.filter((item) => item.value)?.[0]?.value;
2!
359
        }
360
        handleInterceptEvent({ eventData, executeEvent, viewConfig: this.viewConfig, getUnsavedAnswerTml });
10✔
361
    }
362

363
    private messageEventListener = (event: MessageEvent<any>) => {
495✔
364
        const eventType = this.getEventType(event);
3,155✔
365
        const eventPort = this.getEventPort(event);
3,155✔
366
        const eventData = this.formatEventData(event, eventType);
3,155✔
367
        if (event.source === this.iFrame.contentWindow) {
3,155✔
368
            const processedEventData = processEventData(
74✔
369
                eventType,
370
                eventData,
371
                this.thoughtSpotHost,
372
                this.isPreRendered ? this.preRenderWrapper : this.el,
74✔
373
            );
374

375
            if (eventType === EmbedEvent.ApiIntercept) {
74✔
376
                this.handleApiInterceptEvent({ eventData, eventPort });
10✔
377
                return;
10✔
378
            }
379

380
            this.executeCallbacks(
64✔
381
                eventType,
382
                processedEventData,
383
                eventPort,
384
            );
385
        }
386
    };
387
    /**
388
     * Subscribe to message events that depend on successful iframe setup
389
     */
390
    private subscribeToMessageEvents() {
391
        this.unsubscribeToMessageEvents();
445✔
392

393
        window.addEventListener('message', this.messageEventListener);
445✔
394

395
        this.subscribedListeners.message = this.messageEventListener;
445✔
396
    }
397
   
398

399
    /**
400
     * Adds event listeners for both network and message events.
401
     * This maintains backward compatibility with the existing method.
402
     * Adds a global event listener to window for "message" events.
403
     * ThoughtSpot detects if a particular event is targeted to this
404
     * embed instance through an identifier contained in the payload,
405
     * and executes the registered callbacks accordingly.
406
     */
407
    private subscribeToEvents() {
408
        this.subscribeToNetworkEvents();
11✔
409
        this.subscribeToMessageEvents();
11✔
410
    }
411

412

413
    private unsubscribeToNetworkEvents() {
414
        if (this.subscribedListeners.online) {
453✔
415
            window.removeEventListener('online', this.subscribedListeners.online);
5✔
416
            delete this.subscribedListeners.online;
5✔
417
        }
418
        if (this.subscribedListeners.offline) {
453✔
419
            window.removeEventListener('offline', this.subscribedListeners.offline);
5✔
420
            delete this.subscribedListeners.offline;
5✔
421
        }
422
    }
423

424
    private unsubscribeToMessageEvents() {
425
        if (this.subscribedListeners.message) {
446✔
426
            window.removeEventListener('message', this.subscribedListeners.message);
6✔
427
            delete this.subscribedListeners.message;
6✔
428
        }
429
    }
430

431
    private unsubscribeToEvents() {
432
        Object.keys(this.subscribedListeners).forEach((key) => {
51✔
433
            window.removeEventListener(key, this.subscribedListeners[key]);
125✔
434
        });
435
    }
436

437
    protected async getAuthTokenForCookielessInit() {
438
        let authToken = '';
27✔
439
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
27✔
440

441
        try {
6✔
442
            authToken = await getAuthenticationToken(this.embedConfig);
6✔
443
        } catch (e) {
444
            processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
2✔
445
            throw e;
2✔
446
        }
447

448
        return authToken;
4✔
449
    }
450

451
    protected async getDefaultAppInitData(): Promise<DefaultAppInitData> {
452
        const authToken = await this.getAuthTokenForCookielessInit();
26✔
453
        const customActionsResult = getCustomActions([
24✔
454
            ...(this.viewConfig.customActions || []),
47✔
455
            ...(this.embedConfig.customActions || [])
48✔
456
        ]);
457
        if (customActionsResult.errors.length > 0) {
24!
UNCOV
458
            this.handleError({
×
459
                    errorType: ErrorDetailsTypes.VALIDATION_ERROR,
460
                    message: customActionsResult.errors,
461
                    code: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION,
462
                    error : { type: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION, message: customActionsResult.errors }
463
                });
464
        }
465
        const baseInitData = {
24✔
466
            customisations: getCustomisations(this.embedConfig, this.viewConfig),
467
            authToken,
468
            runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
24✔
469
                ? getRuntimeFilters(this.viewConfig.runtimeFilters)
470
                : null,
471
            runtimeParameterParams: this.viewConfig.excludeRuntimeParametersfromURL
24✔
472
                ? getRuntimeParameters(this.viewConfig.runtimeParameters || [])
1!
473
                : null,
474
            hiddenHomepageModules: this.viewConfig.hiddenHomepageModules || [],
47✔
475
            reorderedHomepageModules: this.viewConfig.reorderedHomepageModules || [],
47✔
476
            hostConfig: this.embedConfig.hostConfig,
477
            hiddenHomeLeftNavItems: this.viewConfig?.hiddenHomeLeftNavItems
96!
478
                ? this.viewConfig?.hiddenHomeLeftNavItems
3!
479
                : [],
480
            customVariablesForThirdPartyTools:
481
                this.embedConfig.customVariablesForThirdPartyTools || {},
38✔
482
            hiddenListColumns: this.viewConfig.hiddenListColumns || [],
48✔
483
            customActions: customActionsResult.actions,
484
            ...getInterceptInitData(this.viewConfig),
485
            ...getHostEventsConfig(this.viewConfig),
486
        };
487

488
        return baseInitData;
24✔
489
    }
490

491
    protected async getAppInitData() {
492
        return this.getDefaultAppInitData();
26✔
493
    }
494

495
    /**
496
     * Send Custom style as part of payload of APP_INIT
497
     * @param _
498
     * @param responder
499
     */
500
    private appInitCb = async (_: any, responder: any) => {
495✔
501
        try {
21✔
502
            const appInitData = await this.getAppInitData();
21✔
503
            this.isAppInitialized = true;
19✔
504
            responder({
19✔
505
                type: EmbedEvent.APP_INIT,
506
                data: appInitData,
507
            });
508
        } catch (e) {
509
            logger.error(`AppInit failed, Error : ${e?.message}`);
2!
510
        }
511
    };
512

513
    /**
514
     * Sends updated auth token to the iFrame to avoid user logout
515
     * @param _
516
     * @param responder
517
     */
518
    private updateAuthToken = async (_: any, responder: any) => {
495✔
519
        const { authType } = this.embedConfig;
10✔
520
        let { autoLogin } = this.embedConfig;
10✔
521
        // Default autoLogin: true for cookieless if undefined/null, otherwise
522
        // false
523
        autoLogin = autoLogin ?? (authType === AuthType.TrustedAuthTokenCookieless);
10✔
524
        if (autoLogin && authType === AuthType.TrustedAuthTokenCookieless) {
10✔
525
            try {
5✔
526
                const authToken = await getAuthenticationToken(this.embedConfig);
5✔
527
                responder({
3✔
528
                    type: EmbedEvent.AuthExpire,
529
                    data: { authToken },
530
                });
531
            } catch (e) {
532
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
2!
533
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
2✔
534
            }
535
        } else if (autoLogin) {
5✔
536
            handleAuth();
2✔
537
        }
538
        notifyAuthFailure(AuthFailureType.EXPIRY);
10✔
539
    };
540

541
    /**
542
     * Auto Login and send updated authToken to the iFrame to avoid user session logout
543
     * @param _
544
     * @param responder
545
     */
546
    private idleSessionTimeout = (_: any, responder: any) => {
495✔
547
        handleAuth().then(async () => {
3✔
548
            let authToken = '';
3✔
549
            try {
3✔
550
                authToken = await getAuthenticationToken(this.embedConfig);
3✔
551
                responder({
2✔
552
                    type: EmbedEvent.IdleSessionTimeout,
553
                    data: { authToken },
554
                });
555
            } catch (e) {
556
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
1!
557
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
1!
558
            }
559
        }).catch((e) => {
UNCOV
560
            logger.error(`Auto Login failed, Error : ${e?.message}`);
×
561
        });
562
        notifyAuthFailure(AuthFailureType.IDLE_SESSION_TIMEOUT);
3✔
563
    };
564

565
    /**
566
     * Register APP_INIT event and sendback init payload
567
     */
568
    private registerAppInit = () => {
495✔
569
        this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
495✔
570
        this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
495✔
571
        this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
495✔
572

573
        const embedListenerReadyHandler = this.createEmbedContainerHandler(EmbedEvent.EmbedListenerReady);
495✔
574
        this.on(EmbedEvent.EmbedListenerReady, embedListenerReadyHandler, { start: false }, true);
495✔
575

576
        const authInitHandler = this.createEmbedContainerHandler(EmbedEvent.AuthInit);
495✔
577
        this.on(EmbedEvent.AuthInit, authInitHandler, { start: false }, true);
495✔
578
    };
579

580
    /**
581
     * Constructs the base URL string to load the ThoughtSpot app.
582
     * @param query
583
     */
584
    protected getEmbedBasePath(query: string): string {
585
        let queryString = query.startsWith('?') ? query : `?${query}`;
147✔
586
        if (this.shouldEncodeUrlQueryParams) {
147✔
587
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
1✔
588
                queryString.substr(1),
589
            )}`;
590
        }
591
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
147✔
592
            .filter((x) => x.length > 0)
441✔
593
            .join('/');
594

595
        return `${basePath}#`;
147✔
596
    }
597

598
    protected async getUpdateEmbedParamsObject() {
599
        let queryParams = this.getEmbedParamsObject();
5✔
600
        const appInitData = await this.getAppInitData();
5✔
601
        queryParams = { ...this.viewConfig, ...queryParams, ...appInitData };
5✔
602
        
603
        return queryParams;
5✔
604
    }
605

606
    /**
607
     * Common query params set for all the embed modes.
608
     * @param queryParams
609
     * @returns queryParams
610
     */
611
    protected getBaseQueryParams(queryParams: Record<any, any> = {}) {
162✔
612
        let hostAppUrl = window?.location?.host || '';
456!
613

614
        // The below check is needed because TS Cloud firewall, blocks
615
        // localhost/127.0.0.1 in any url param.
616
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
456!
617
            hostAppUrl = 'local-host';
456✔
618
        }
619
        const blockNonEmbedFullAppAccess = this.embedConfig.blockNonEmbedFullAppAccess ?? true;
456✔
620
        queryParams[Param.EmbedApp] = true;
456✔
621
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
456✔
622
        queryParams[Param.ViewPortHeight] = window.innerHeight;
456✔
623
        queryParams[Param.ViewPortWidth] = window.innerWidth;
456✔
624
        queryParams[Param.Version] = version;
456✔
625
        queryParams[Param.AuthType] = this.embedConfig.authType;
456✔
626
        queryParams[Param.blockNonEmbedFullAppAccess] = blockNonEmbedFullAppAccess;
456✔
627
        queryParams[Param.AutoLogin] = this.embedConfig.autoLogin;
456✔
628
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
456✔
629
            queryParams[Param.DisableLoginRedirect] = true;
8✔
630
        }
631
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
456!
UNCOV
632
            queryParams[Param.ForceSAMLAutoRedirect] = true;
×
633
        }
634
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
456✔
635
            queryParams[Param.cookieless] = true;
16✔
636
        }
637
        if (this.embedConfig.pendoTrackingKey) {
456✔
638
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
1✔
639
        }
640
        if (this.embedConfig.numberFormatLocale) {
456✔
641
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
10✔
642
        }
643
        if (this.embedConfig.dateFormatLocale) {
456✔
644
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
10✔
645
        }
646
        if (this.embedConfig.currencyFormat) {
456✔
647
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
10✔
648
        }
649

650
        const {
651
            disabledActions,
652
            disabledActionReason,
653
            hiddenActions,
654
            visibleActions,
655
            hiddenTabs,
656
            visibleTabs,
657
            showAlerts,
658
            additionalFlags: additionalFlagsFromView,
659
            locale,
660
            customizations,
661
            contextMenuTrigger,
662
            linkOverride,
663
            insertInToSlide,
664
            disableRedirectionLinksInNewTab,
665
            overrideOrgId,
666
            exposeTranslationIDs,
667
            primaryAction,
668
        } = this.viewConfig;
456✔
669

670
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
456✔
671

672
        const additionalFlags = {
456✔
673
            ...additionalFlagsFromInit,
674
            ...additionalFlagsFromView,
675
        };
676

677
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
456✔
678
            this.handleError({
4✔
679
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
680
                message: ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
681
                code: EmbedErrorCodes.CONFLICTING_ACTIONS_CONFIG,
682
                error : ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
683
            });
684
            return queryParams;
4✔
685
        }
686

687
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
452✔
688
            this.handleError({
4✔
689
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
690
                message: ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
691
                code: EmbedErrorCodes.CONFLICTING_TABS_CONFIG,
692
                error : ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
693
            });
694
            return queryParams;
4✔
695
        }
696
        if (primaryAction) {
448!
UNCOV
697
            queryParams[Param.PrimaryAction] = primaryAction;
×
698
        }
699

700
        if (disabledActions?.length) {
448✔
701
            queryParams[Param.DisableActions] = disabledActions;
7✔
702
        }
703
        if (disabledActionReason) {
448✔
704
            queryParams[Param.DisableActionReason] = disabledActionReason;
6✔
705
        }
706
        if (exposeTranslationIDs) {
448✔
707
            queryParams[Param.ExposeTranslationIDs] = exposeTranslationIDs;
1✔
708
        }
709
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
448✔
710
        if (Array.isArray(visibleActions)) {
448✔
711
            queryParams[Param.VisibleActions] = visibleActions;
6✔
712
        }
713
        if (Array.isArray(hiddenTabs)) {
448✔
714
            queryParams[Param.HiddenTabs] = hiddenTabs;
2✔
715
        }
716
        if (Array.isArray(visibleTabs)) {
448✔
717
            queryParams[Param.VisibleTabs] = visibleTabs;
1✔
718
        }
719
        /**
720
         * Default behavior for context menu will be left-click
721
         *  from version 9.2.0.cl the user have an option to override context
722
         *  menu click
723
         */
724
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
448✔
725
            queryParams[Param.ContextMenuTrigger] = 'left';
3✔
726
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
445✔
727
            queryParams[Param.ContextMenuTrigger] = 'right';
2✔
728
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.BOTH_CLICKS) {
443✔
729
            queryParams[Param.ContextMenuTrigger] = 'both';
2✔
730
        }
731

732
        const embedCustomizations = this.embedConfig.customizations;
448✔
733
        const spriteUrl = customizations?.iconSpriteUrl || embedCustomizations?.iconSpriteUrl;
448✔
734
        if (spriteUrl) {
448✔
735
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
1✔
736
        }
737

738
        const stringIDsUrl = customizations?.content?.stringIDsUrl
448✔
739
            || embedCustomizations?.content?.stringIDsUrl;
2,688✔
740
        if (stringIDsUrl) {
448✔
741
            queryParams[Param.StringIDsUrl] = stringIDsUrl;
2✔
742
        }
743

744
        if (showAlerts !== undefined) {
448✔
745
            queryParams[Param.ShowAlerts] = showAlerts;
1✔
746
        }
747
        if (locale !== undefined) {
448✔
748
            queryParams[Param.Locale] = locale;
1✔
749
        }
750

751
        if (linkOverride) {
448!
UNCOV
752
            queryParams[Param.LinkOverride] = linkOverride;
×
753
        }
754
        if (insertInToSlide) {
448!
UNCOV
755
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
×
756
        }
757
        if (disableRedirectionLinksInNewTab) {
448!
UNCOV
758
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
×
759
        }
760
        if (overrideOrgId !== undefined) {
448✔
761
            queryParams[Param.OverrideOrgId] = overrideOrgId;
3✔
762
        }
763

764
        if (this.isPreAuthCacheEnabled()) {
448✔
765
            queryParams[Param.preAuthCache] = true;
439✔
766
        }
767

768
        queryParams[Param.OverrideNativeConsole] = true;
448✔
769
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
448✔
770

771
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
448✔
772
            Object.assign(queryParams, additionalFlags);
8✔
773
        }
774

775
        // Do not add any flags below this, as we want additional flags to
776
        // override other flags
777

778
        return queryParams;
448✔
779
    }
780

781
    /**
782
     * Constructs the base URL string to load v1 of the ThoughtSpot app.
783
     * This is used for embedding Liveboards, visualizations, and full application.
784
     * @param queryString The query string to append to the URL.
785
     * @param isAppEmbed A Boolean parameter to specify if you are embedding
786
     * the full application.
787
     */
788
    protected getV1EmbedBasePath(queryString: string): string {
789
        const queryParams = this.shouldEncodeUrlQueryParams
304✔
790
            ? `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString)}`
791
            : `?${queryString}`;
792
        const host = this.thoughtSpotHost;
304✔
793
        const path = `${host}/${queryParams}#`;
304✔
794
        return path;
304✔
795
    }
796

797
    protected getEmbedParams() {
UNCOV
798
        const queryParams = this.getEmbedParamsObject();
×
799
        return getQueryParamString(queryParams);
×
800
    }
801

802
    protected getEmbedParamsObject() {
UNCOV
803
        const params = this.getBaseQueryParams();
×
804
        return params;
×
805
    }
806

807
    protected getRootIframeSrc() {
808
        const query = this.getEmbedParams();
114✔
809
        return this.getEmbedBasePath(query);
114✔
810
    }
811

812
    protected createIframeEl(frameSrc: string): HTMLIFrameElement {
813
        const iFrame = document.createElement('iframe');
425✔
814

815
        iFrame.src = frameSrc;
425✔
816
        iFrame.id = TS_EMBED_ID;
425✔
817
        iFrame.setAttribute('data-ts-iframe', 'true');
425✔
818

819
        // according to screenfull.js documentation
820
        // allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be
821
        // true
822
        iFrame.allowFullscreen = true;
425✔
823
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
824
        // @ts-ignore
825
        iFrame.webkitallowfullscreen = true;
425✔
826
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
827
        // @ts-ignore
828
        iFrame.mozallowfullscreen = true;
425✔
829
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
830
        // @ts-ignore
831
        iFrame.allow = 'clipboard-read; clipboard-write; fullscreen;';
425✔
832

833
        const frameParams = this.viewConfig.frameParams;
425✔
834
        const { height: frameHeight, width: frameWidth, ...restParams } = frameParams || {};
425✔
835
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
425✔
836
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
425✔
837
        setAttributes(iFrame, restParams);
425✔
838

839
        iFrame.style.width = `${width}`;
425✔
840
        iFrame.style.height = `${height}`;
425✔
841
        // Set minimum height to the frame so that,
842
        // scaling down on the fullheight doesn't make it too small.
843
        iFrame.style.minHeight = `${height}`;
425✔
844
        iFrame.style.border = '0';
425✔
845
        iFrame.name = 'ThoughtSpot Embedded Analytics';
425✔
846
        return iFrame;
425✔
847
    }
848

849
    protected handleInsertionIntoDOM(child: string | Node): void {
850
        if (this.isPreRendered) {
442✔
851
            this.insertIntoDOMForPreRender(child);
20✔
852
        } else {
853
            this.insertIntoDOM(child);
422✔
854
        }
855
        if (this.insertedDomEl instanceof Node) {
442✔
856
            (this.insertedDomEl as any)[this.embedNodeKey] = this;
436✔
857
        }
858
    }
859

860
    /**
861
     * Renders the embedded ThoughtSpot app in an iframe and sets up
862
     * event listeners.
863
     * @param url - The URL of the embedded ThoughtSpot app.
864
     */
865
    protected async renderIFrame(url: string): Promise<any> {
866
        if (this.isError) {
450✔
867
            return null;
8✔
868
        }
869
        if (!this.thoughtSpotHost) {
442✔
870
            this.throwInitError();
1✔
871
        }
872
        if (url.length > URL_MAX_LENGTH) {
442!
873
            // warn: The URL is too long
874
        }
875

876
        return renderInQueue((nextInQueue) => {
442✔
877
            const initTimestamp = Date.now();
442✔
878

879
            this.executeCallbacks(EmbedEvent.Init, {
442✔
880
                data: {
881
                    timestamp: initTimestamp,
882
                },
883
                type: EmbedEvent.Init,
884
            });
885

886
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
442✔
887

888
            // Always subscribe to network events, regardless of auth status
889
            this.subscribeToNetworkEvents();
442✔
890

891
            return getAuthPromise()
442!
892
                ?.then((isLoggedIn: boolean) => {
893
                    if (!isLoggedIn) {
437✔
894
                        this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
3✔
895
                        return;
3✔
896
                    }
897

898
                    this.setIframeElement(this.iFrame || this.createIframeEl(url));
434✔
899
                    this.iFrame.addEventListener('load', () => {
434✔
900
                        nextInQueue();
17✔
901
                        const loadTimestamp = Date.now();
17✔
902
                        this.executeCallbacks(EmbedEvent.Load, {
17✔
903
                            data: {
904
                                timestamp: loadTimestamp,
905
                            },
906
                            type: EmbedEvent.Load,
907
                        });
908
                        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE, {
17✔
909
                            elWidth: this.iFrame.clientWidth,
910
                            elHeight: this.iFrame.clientHeight,
911
                            timeTookToLoad: loadTimestamp - initTimestamp,
912
                        });
913
                        // Send info event  if preauth cache is enabled
914
                        if (this.isPreAuthCacheEnabled()) {
17✔
915
                            getPreauthInfo().then((data) => {
13✔
916
                                if (data?.info) {
13✔
917
                                    this.trigger(HostEvent.InfoSuccess, data);
5✔
918
                                }
919
                            });
920
                        }
921

922
                        // Setup fullscreen change handler after iframe is
923
                        // loaded and ready
924
                        this.setupFullscreenChangeHandler();
17✔
925
                    });
926
                    this.iFrame.addEventListener('error', () => {
434✔
927
                        nextInQueue();
1✔
928
                    });
929
                    this.handleInsertionIntoDOM(this.iFrame);
434✔
930
                    const prefetchIframe = document.querySelectorAll('.prefetchIframe');
434✔
931
                    if (prefetchIframe.length) {
434!
UNCOV
932
                        prefetchIframe.forEach((el) => {
×
933
                            el.remove();
×
934
                        });
935
                    }
936
                    // Subscribe to message events only after successful
937
                    // auth and iframe setup
938
                    this.subscribeToMessageEvents();
434✔
939
                })
940
                .catch((error) => {
941
                    nextInQueue();
5✔
942
                    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_FAILED, {
5✔
943
                        error: JSON.stringify(error),
944
                    });
945
                    this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
5✔
946
                    this.handleError({
5✔
947
                        errorType: ErrorDetailsTypes.API,
948
                        message: error.message || ERROR_MESSAGE.LOGIN_FAILED,
6✔
949
                        code: EmbedErrorCodes.LOGIN_FAILED,
950
                        error : error,
951
                    });
952
                });
953
        });
954
    }
955

956
    protected createPreRenderWrapper(): HTMLDivElement {
957
        const preRenderIds = this.getPreRenderIds();
20✔
958

959
        document.getElementById(preRenderIds.wrapper)?.remove();
20✔
960

961
        const preRenderWrapper = document.createElement('div');
20✔
962
        preRenderWrapper.id = preRenderIds.wrapper;
20✔
963
        const initialPreRenderWrapperStyle = {
20✔
964
            position: 'absolute',
965
            top: '0',
966
            left: '0',
967
            width: '100vw',
968
            height: '100vh',
969
        };
970
        setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
20✔
971

972
        return preRenderWrapper;
20✔
973
    }
974

975
    protected preRenderWrapper: HTMLElement;
976

977
    protected preRenderChild: HTMLElement;
978

979
    protected connectPreRendered(): boolean {
980
        const preRenderIds = this.getPreRenderIds();
30✔
981
        const preRenderWrapperElement = document.getElementById(preRenderIds.wrapper);
30✔
982
        this.preRenderWrapper = this.preRenderWrapper || preRenderWrapperElement;
30✔
983

984
        this.preRenderChild = this.preRenderChild || document.getElementById(preRenderIds.child);
30✔
985

986
        if (this.preRenderWrapper && this.preRenderChild) {
30✔
987
            this.isPreRendered = true;
10✔
988
            if (this.preRenderChild instanceof HTMLIFrameElement) {
10✔
989
                this.setIframeElement(this.preRenderChild);
10✔
990
            }
991
            this.insertedDomEl = this.preRenderWrapper;
10✔
992
            this.isRendered = true;
10✔
993
        }
994

995
        return this.isPreRenderAvailable();
30✔
996
    }
997

998
    protected isPreRenderAvailable(): boolean {
999
        return (
76✔
1000
            this.isRendered
166✔
1001
            && this.isPreRendered
1002
            && Boolean(this.preRenderWrapper && this.preRenderChild)
90✔
1003
        );
1004
    }
1005

1006
    protected createPreRenderChild(child: string | Node): HTMLElement {
1007
        const preRenderIds = this.getPreRenderIds();
20✔
1008

1009
        document.getElementById(preRenderIds.child)?.remove();
20✔
1010

1011
        if (child instanceof HTMLElement) {
20✔
1012
            child.id = preRenderIds.child;
19✔
1013
            return child;
19✔
1014
        }
1015

1016
        const divChildNode = document.createElement('div');
1✔
1017
        setStyleProperties(divChildNode, { width: '100%', height: '100%' });
1✔
1018
        divChildNode.id = preRenderIds.child;
1✔
1019

1020
        if (typeof child === 'string') {
1!
1021
            divChildNode.innerHTML = child;
1✔
1022
        } else {
UNCOV
1023
            divChildNode.appendChild(child);
×
1024
        }
1025

1026
        return divChildNode;
1✔
1027
    }
1028

1029
    protected insertIntoDOMForPreRender(child: string | Node): void {
1030
        const preRenderChild = this.createPreRenderChild(child);
20✔
1031
        const preRenderWrapper = this.createPreRenderWrapper();
20✔
1032
        preRenderWrapper.appendChild(preRenderChild);
20✔
1033

1034
        this.preRenderChild = preRenderChild;
20✔
1035
        this.preRenderWrapper = preRenderWrapper;
20✔
1036

1037
        if (preRenderChild instanceof HTMLIFrameElement) {
20✔
1038
            this.setIframeElement(preRenderChild);
19✔
1039
        }
1040
        this.insertedDomEl = preRenderWrapper;
20✔
1041

1042
        if (this.showPreRenderByDefault) {
20✔
1043
            this.showPreRender();
2✔
1044
        } else {
1045
            this.hidePreRender();
18✔
1046
        }
1047

1048
        document.body.appendChild(preRenderWrapper);
20✔
1049
    }
1050

1051
    private showPreRenderByDefault = false;
495✔
1052

1053
    protected insertIntoDOM(child: string | Node): void {
1054
        if (this.viewConfig.insertAsSibling) {
422✔
1055
            if (typeof child === 'string') {
12✔
1056
                const div = document.createElement('div');
1✔
1057
                div.innerHTML = child;
1✔
1058
                div.id = TS_EMBED_ID;
1✔
1059

1060
                child = div;
1✔
1061
            }
1062
            if (this.el.nextElementSibling?.id === TS_EMBED_ID) {
12✔
1063
                this.el.nextElementSibling.remove();
1✔
1064
            }
1065
            this.el.parentElement.insertBefore(child, this.el.nextSibling);
12✔
1066
            this.insertedDomEl = child;
12✔
1067
        } else if (typeof child === 'string') {
410✔
1068
            this.el.innerHTML = child;
6✔
1069
            this.insertedDomEl = this.el.children[0];
6✔
1070
        } else {
1071
            this.el.innerHTML = '';
404✔
1072
            this.el.appendChild(child);
404✔
1073
            this.insertedDomEl = child;
404✔
1074
        }
1075
    }
1076

1077
    /**
1078
     * Sets the height of the iframe
1079
     * @param height The height in pixels
1080
     */
1081
    protected setIFrameHeight(height: number | string): void {
1082
        this.iFrame.style.height = getCssDimension(height);
3✔
1083
    }
1084

1085
    /**
1086
     * We can process the customer given payload before sending it to the embed port
1087
     * Embed event handler -> responder -> createEmbedEventResponder -> send response
1088
     * @param eventPort The event port for a specific MessageChannel
1089
     * @param eventType The event type
1090
     * @returns 
1091
     */
1092
    protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => {
495✔
1093

1094
        const getPayloadToSend = (payload: any) => {
64✔
1095
            if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept) {
29✔
1096
                return processLegacyInterceptResponse(payload);
1✔
1097
            }
1098
            if (eventType === EmbedEvent.ApiIntercept) {
28!
UNCOV
1099
                return processApiInterceptResponse(payload);
×
1100
            }
1101
            return payload;
28✔
1102
        }   
1103
        return (payload: any) => {
64✔
1104
            const payloadToSend = getPayloadToSend(payload);
29✔
1105
            this.triggerEventOnPort(eventPort, payloadToSend);
29✔
1106
        }
1107
    }
1108

1109
    /**
1110
     * Executes all registered event handlers for a particular event type
1111
     * @param eventType The event type
1112
     * @param data The payload invoked with the event handler
1113
     * @param eventPort The event Port for a specific MessageChannel
1114
     */
1115
    protected executeCallbacks(
1116
        eventType: EmbedEvent,
1117
        data: any,
1118
        eventPort?: MessagePort | void,
1119
    ): void {
1120
        const eventHandlers = this.eventHandlerMap.get(eventType) || [];
543✔
1121
        const allHandlers = this.eventHandlerMap.get(EmbedEvent.ALL) || [];
543✔
1122
        const callbacks = [...eventHandlers, ...allHandlers];
543✔
1123
        const dataStatus = data?.status || embedEventStatus.END;
543!
1124
        callbacks.forEach((callbackObj) => {
543✔
1125
            if (
64✔
1126
                // When start status is true it trigger only start releated
1127
                // payload
1128
                (callbackObj.options.start && dataStatus === embedEventStatus.START)
191✔
1129
                // When start status is false it trigger only end releated
1130
                // payload
1131
                || (!callbackObj.options.start && dataStatus === embedEventStatus.END)
1132
            ) {
1133
                const responder = this.createEmbedEventResponder(eventPort, eventType);
63✔
1134
                callbackObj.callback(data, responder);
63✔
1135
            }
1136
        });
1137
    }
1138

1139
    /**
1140
     * Returns the ThoughtSpot hostname or IP address.
1141
     */
1142
    protected getThoughtSpotHost(): string {
UNCOV
1143
        return this.thoughtSpotHost;
×
1144
    }
1145

1146
    /**
1147
     * Gets the v1 event type (if applicable) for the EmbedEvent type
1148
     * @param eventType The v2 event type
1149
     * @returns The corresponding v1 event type if one exists
1150
     * or else the v2 event type itself
1151
     */
1152
    protected getCompatibleEventType(eventType: EmbedEvent): EmbedEvent {
1153
        return V1EventMap[eventType] || eventType;
1,745✔
1154
    }
1155

1156
    /**
1157
     * Calculates the iframe center for the current visible viewPort
1158
     * of iframe using Scroll position of Host App, offsetTop for iframe
1159
     * in Host app. ViewPort height of the tab.
1160
     * @returns iframe Center in visible viewport,
1161
     *  Iframe height,
1162
     *  View port height.
1163
     */
1164
    protected getIframeCenter() {
1165
        const offsetTopClient = getOffsetTop(this.iFrame);
5✔
1166
        const scrollTopClient = window.scrollY;
5✔
1167
        const viewPortHeight = window.innerHeight;
5✔
1168
        const iframeHeight = this.iFrame.offsetHeight;
5✔
1169
        const iframeScrolled = scrollTopClient - offsetTopClient;
5✔
1170
        let iframeVisibleViewPort;
1171
        let iframeOffset;
1172

1173
        if (iframeScrolled < 0) {
5!
UNCOV
1174
            iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
×
1175
            iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
×
1176
            iframeOffset = 0;
×
1177
        } else {
1178
            iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
5✔
1179
            iframeOffset = iframeScrolled;
5✔
1180
        }
1181
        const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
5✔
1182
        return {
5✔
1183
            iframeCenter,
1184
            iframeScrolled,
1185
            iframeHeight,
1186
            viewPortHeight,
1187
            iframeVisibleViewPort,
1188
        };
1189
    }
1190

1191
    /**
1192
     * Registers an event listener to trigger an alert when the ThoughtSpot app
1193
     * sends an event of a particular message type to the host application.
1194
     * @param messageType The message type
1195
     * @param callback A callback as a function
1196
     * @param options The message options
1197
     * @param isSelf
1198
     * @param isRegisteredBySDK
1199
     * @example
1200
     * ```js
1201
     * tsEmbed.on(EmbedEvent.Error, (data) => {
1202
     *   console.error(data);
1203
     * });
1204
     * ```
1205
     * @example
1206
     * ```js
1207
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1208
     *   console.log("Answer save clicked", data);
1209
     * }, {
1210
     *   start: true // This will trigger the callback on start of save
1211
     * });
1212
     * ```
1213
     */
1214
    public on(
1215
        messageType: EmbedEvent,
1216
        callback: MessageCallback,
1217
        options: MessageOptions = { start: false },
17✔
1218
        isRegisteredBySDK = false,
1,764✔
1219
    ): typeof TsEmbed.prototype {
1220
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_ON}-${messageType}`, {
2,609✔
1221
            isRegisteredBySDK,
1222
        });
1223
        if (this.isRendered) {
2,609✔
1224
            logger.warn('Please register event handlers before calling render');
2✔
1225
        }
1226
        const callbacks = this.eventHandlerMap.get(messageType) || [];
2,609✔
1227
        callbacks.push({ options, callback });
2,609✔
1228
        this.eventHandlerMap.set(messageType, callbacks);
2,609✔
1229
        return this;
2,609✔
1230
    }
1231

1232
    /**
1233
     * Removes an event listener for a particular event type.
1234
     * @param messageType The message type
1235
     * @param callback The callback to remove
1236
     * @example
1237
     * ```js
1238
     * const errorHandler = (data) => { console.error(data); };
1239
     * tsEmbed.on(EmbedEvent.Error, errorHandler);
1240
     * tsEmbed.off(EmbedEvent.Error, errorHandler);
1241
     * ```
1242
     */
1243
    public off(messageType: EmbedEvent, callback: MessageCallback): typeof TsEmbed.prototype {
1244
        const callbacks = this.eventHandlerMap.get(messageType) || [];
1!
1245
        const index = callbacks.findIndex((cb) => cb.callback === callback);
1✔
1246
        if (index > -1) {
1✔
1247
            callbacks.splice(index, 1);
1✔
1248
        }
1249
        return this;
1✔
1250
    }
1251

1252
    /**
1253
     * Triggers an event on specific Port registered against
1254
     * for the EmbedEvent
1255
     * @param eventType The message type
1256
     * @param data The payload to send
1257
     * @param eventPort
1258
     * @param payload
1259
     */
1260
    private triggerEventOnPort(eventPort: MessagePort | void, payload: any) {
1261
        if (eventPort) {
29✔
1262
            try {
25✔
1263
                eventPort.postMessage({
25✔
1264
                    type: payload.type,
1265
                    data: payload.data,
1266
                });
1267
            } catch (e) {
UNCOV
1268
                eventPort.postMessage({ error: e });
×
1269
                logger.log(e);
×
1270
            }
1271
        } else {
1272
            logger.log('Event Port is not defined');
4✔
1273
        }
1274
    }
1275

1276
    /**
1277
    * @hidden
1278
    * Internal state to track if the embed container is loaded.
1279
    * This is used to trigger events after the embed container is loaded.
1280
    */
1281
    public isEmbedContainerLoaded = false;
495✔
1282

1283
    /**
1284
     * @hidden
1285
     * Internal state to track the callbacks to be executed after the embed container 
1286
     * is loaded.
1287
     * This is used to trigger events after the embed container is loaded.
1288
     */
1289
    private embedContainerReadyCallbacks: Array<() => void> = [];
495✔
1290

1291
    protected getPreRenderObj<T extends TsEmbed>(): T {
1292
        const embedObj = (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
40✔
1293
        if (embedObj === (this as any)) {
40✔
1294
            logger.info('embedObj is same as this');
7✔
1295
        }
1296
        return embedObj;
40✔
1297
    }
1298

1299
    private checkEmbedContainerLoaded() {
1300
        if (this.isEmbedContainerLoaded) return true;
37✔
1301

1302
        const preRenderObj = this.getPreRenderObj<TsEmbed>();
27✔
1303
        if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
27✔
1304
            this.isEmbedContainerLoaded = true;
6✔
1305
        }
1306

1307
        return this.isEmbedContainerLoaded;
27✔
1308
    }
1309
    private executeEmbedContainerReadyCallbacks() {
1310
        logger.debug('executePendingEvents', this.embedContainerReadyCallbacks);
14✔
1311
        this.embedContainerReadyCallbacks.forEach((callback) => {
14✔
1312
            callback?.();
12!
1313
        });
1314
        this.embedContainerReadyCallbacks = [];
14✔
1315
    }
1316

1317
    /**
1318
     * Executes a callback after the embed container is loaded.
1319
     * @param callback The callback to execute
1320
     */
1321
    protected executeAfterEmbedContainerLoaded(callback: () => void) {
1322
        if (this.checkEmbedContainerLoaded()) {
36✔
1323
            callback?.();
15!
1324
        } else {
1325
            logger.debug('pushing callback to embedContainerReadyCallbacks', callback);
21✔
1326
            this.embedContainerReadyCallbacks.push(callback);
21✔
1327
        }
1328
    }
1329

1330
    protected createEmbedContainerHandler = (source: EmbedEvent.AuthInit | EmbedEvent.EmbedListenerReady) => () => {
992✔
1331
        const processEmbedContainerReady = () => {
10✔
1332
            logger.debug('processEmbedContainerReady');
9✔
1333
            this.isEmbedContainerLoaded = true;
9✔
1334
            this.executeEmbedContainerReadyCallbacks();
9✔
1335
        }
1336
        if (source === EmbedEvent.AuthInit) {
10✔
1337
            const AUTH_INIT_FALLBACK_DELAY = 1000;
7✔
1338
            // Wait for 1 second to ensure the embed container is loaded
1339
            // This is a workaround to ensure the embed container is loaded
1340
            // this is needed until all clusters have EmbedListenerReady event
1341
            setTimeout(processEmbedContainerReady, AUTH_INIT_FALLBACK_DELAY);
7✔
1342
        } else if (source === EmbedEvent.EmbedListenerReady) {
3✔
1343
            processEmbedContainerReady();
3✔
1344
        }
1345
    }
1346

1347
    /**
1348
     * Triggers an event to the embedded app
1349
     * @param {HostEvent} messageType The event type
1350
     * @param {any} data The payload to send with the message
1351
     * @returns A promise that resolves with the response from the embedded app
1352
     */
1353
    public async trigger<HostEventT extends HostEvent, PayloadT, ContextT extends ContextType>(
1354
        messageType: HostEventT,
1355
        data: TriggerPayload<PayloadT, HostEventT> = {} as any,
222✔
1356
        context?: ContextT,
1357
    ): Promise<TriggerResponse<PayloadT, HostEventT, ContextT>> {
1358
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
325✔
1359

1360
        if (!this.isRendered) {
325!
UNCOV
1361
            this.handleError({
×
1362
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1363
                message: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1364
                code: EmbedErrorCodes.RENDER_NOT_CALLED,
1365
                error: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1366
            });
UNCOV
1367
            return null;
×
1368
        }
1369

1370
        if (!messageType) {
325✔
1371
            this.handleError({
1✔
1372
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1373
                message: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1374
                code: EmbedErrorCodes.HOST_EVENT_TYPE_UNDEFINED,
1375
                error: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1376
            });
1377
            return null;
1✔
1378
        }
1379

1380
        // Check if iframe exists before triggering - 
1381
        // this prevents the error when auth fails
1382
        if (!this.iFrame) {
324✔
1383
            logger.debug(
25✔
1384
                `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
1385
            );
1386
            return null;
25✔
1387
        }
1388

1389
        // send an empty object, this is needed for liveboard default handlers
1390
        return this.hostEventClient.triggerHostEvent(messageType, data, context);
299✔
1391
    }
1392

1393
    /**
1394
     * Triggers an event to the embedded app, skipping the UI flow.
1395
     * @param {UIPassthroughEvent} apiName - The name of the API to be triggered.
1396
     * @param {UIPassthroughRequest} parameters - The parameters to be passed to the API.
1397
     * @returns {Promise<UIPassthroughRequest>} - A promise that resolves with the response
1398
     * from the embedded app.
1399
     */
1400
    public async triggerUIPassThrough<UIPassthroughEventT extends UIPassthroughEvent>(
1401
        apiName: UIPassthroughEventT,
1402
        parameters: UIPassthroughRequest<UIPassthroughEventT>,
1403
    ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> {
1404
        const response = this.hostEventClient.triggerUIPassthroughApi(apiName, parameters);
3✔
1405
        return response;
3✔
1406
    }
1407

1408
    /**
1409
     * Marks the ThoughtSpot object to have been rendered
1410
     * Needs to be overridden by subclasses to do the actual
1411
     * rendering of the iframe.
1412
     * @param args
1413
     */
1414
    public async render(): Promise<TsEmbed> {
1415
        if (!getIsInitCalled()) {
444✔
1416
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
2✔
1417
        }
1418
        await this.isReadyForRenderPromise;
444✔
1419
        this.isRendered = true;
444✔
1420

1421
        return this;
444✔
1422
    }
1423

1424
    public getIframeSrc(): string {
1425
        return '';
1✔
1426
    }
1427

1428
    protected handleRenderForPrerender() {
1429
        return this.render();
15✔
1430
    }
1431

1432
    /**
1433
     * Get the current context of the embedded TS component.
1434
     * @returns The current context object containing the page type and object ids.
1435
     * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl
1436
     */
1437
    public async getCurrentContext(): Promise<any> {
NEW
1438
        return new Promise((resolve) => {
×
NEW
1439
            this.executeAfterEmbedContainerLoaded(async () => {
×
NEW
1440
                const context = await this.trigger(HostEvent.GetPageContext, {});
×
NEW
1441
                resolve(context);
×
1442
            });
1443
        });
1444
    }
1445

1446
    /**
1447
     * Creates the preRender shell
1448
     * @param showPreRenderByDefault - Show the preRender after render, hidden by default
1449
     */
1450

1451
    public async preRender(showPreRenderByDefault = false, replaceExistingPreRender = false): Promise<TsEmbed> {
36✔
1452
        if (!this.viewConfig.preRenderId) {
22✔
1453
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1✔
1454
            return this;
1✔
1455
        }
1456
        this.isPreRendered = true;
21✔
1457
        this.showPreRenderByDefault = showPreRenderByDefault;
21✔
1458

1459
        const isAlreadyRendered = this.connectPreRendered();
21✔
1460
        if (isAlreadyRendered && !replaceExistingPreRender) {
21✔
1461
            return this;
1✔
1462
        }
1463

1464
        return this.handleRenderForPrerender();
20✔
1465
    }
1466

1467
    /**
1468
     * Get the Post Url Params for THOUGHTSPOT from the current
1469
     * host app URL.
1470
     * THOUGHTSPOT URL params starts with a prefix "ts-"
1471
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
1472
     */
1473
    public getThoughtSpotPostUrlParams(
1474
        additionalParams: { [key: string]: string | number } = {},
429✔
1475
    ): string {
1476
        const urlHash = window.location.hash;
453✔
1477
        const queryParams = window.location.search;
453✔
1478
        const postHashParams = urlHash.split('?');
453✔
1479
        const postURLParams = postHashParams[postHashParams.length - 1];
453✔
1480
        const queryParamsObj = new URLSearchParams(queryParams);
453✔
1481
        const postURLParamsObj = new URLSearchParams(postURLParams);
453✔
1482
        const params = new URLSearchParams();
453✔
1483

1484
        const addKeyValuePairCb = (value: string, key: string): void => {
453✔
1485
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
8✔
1486
                params.append(key, value);
5✔
1487
            }
1488
        };
1489
        queryParamsObj.forEach(addKeyValuePairCb);
453✔
1490
        postURLParamsObj.forEach(addKeyValuePairCb);
453✔
1491
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
453✔
1492

1493
        let tsParams = params.toString();
453✔
1494
        tsParams = tsParams ? `?${tsParams}` : '';
453✔
1495

1496
        return tsParams;
453✔
1497
    }
1498

1499
    /**
1500
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
1501
     * @version SDK: 1.19.1 | ThoughtSpot: *
1502
     */
1503
    public destroy(): void {
1504
        try {
31✔
1505
            this.removeFullscreenChangeHandler();
31✔
1506
            this.unsubscribeToEvents();
31✔
1507
            if (!getEmbedConfig().waitForCleanupOnDestroy) {
31✔
1508
                this.trigger(HostEvent.DestroyEmbed)
28✔
1509
                this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
28✔
1510
            } else {
1511
                const cleanupTimeout = getEmbedConfig().cleanupTimeout;
3✔
1512
                Promise.race([
3✔
1513
                    this.trigger(HostEvent.DestroyEmbed),
1514
                    new Promise((resolve) => setTimeout(resolve, cleanupTimeout)),
3✔
1515
                ]).then(() => {
1516
                    this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
3!
1517
                }).catch((e) => {
UNCOV
1518
                    logger.log('Error destroying TS Embed', e);
×
1519
                });
1520
            }
1521
        } catch (e) {
1522
            logger.log('Error destroying TS Embed', e);
1✔
1523
        }
1524
    }
1525

1526
    public getUnderlyingFrameElement(): HTMLIFrameElement {
1527
        return this.iFrame;
1✔
1528
    }
1529

1530
    /**
1531
     * Prerenders a generic instance of the TS component.
1532
     * This means without the path but with the flags already applied.
1533
     * This is useful for prerendering the component in the background.
1534
     * @version SDK: 1.22.0
1535
     * @returns
1536
     */
1537
    public async prerenderGeneric(): Promise<any> {
1538
        if (!getIsInitCalled()) {
8✔
1539
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1✔
1540
        }
1541
        await this.isReadyForRenderPromise;
8✔
1542

1543
        const prerenderFrameSrc = this.getRootIframeSrc();
8✔
1544
        this.isRendered = true;
8✔
1545
        return this.renderIFrame(prerenderFrameSrc);
8✔
1546
    }
1547

1548
    protected beforePrerenderVisible(): void {
1549
        // Override in subclass
1550
    }
1551

1552
    private validatePreRenderViewConfig = (viewConfig: ViewConfig) => {
495✔
1553
        const preRenderAllowedKeys = ['preRenderId', 'vizId', 'liveboardId'];
7✔
1554
        const preRenderedObject = (this.insertedDomEl as any)?.[this.embedNodeKey] as TsEmbed;
7!
1555
        if (!preRenderedObject) return;
7!
1556
        if (viewConfig.preRenderId) {
7✔
1557
            const allOtherKeys = Object.keys(viewConfig).filter(
7✔
1558
                (key) => !preRenderAllowedKeys.includes(key) && !key.startsWith('on'),
38✔
1559
            );
1560

1561
            allOtherKeys.forEach((key: keyof ViewConfig) => {
7✔
1562
                if (
22✔
1563
                    !isUndefined(viewConfig[key])
44✔
1564
                    && !isEqual(viewConfig[key], preRenderedObject.viewConfig[key])
1565
                ) {
1566
                    logger.warn(
8✔
1567
                        `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
8!
1568
                        + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1569
                        + `but a different value "${JSON.stringify(viewConfig[key])}" `
1570
                        + 'was passed to the Embed component. '
1571
                        + 'The new value provided is ignored, the value provided during '
1572
                        + 'preRender is used.',
1573
                    );
1574
                }
1575
            });
1576
        }
1577
    };
1578

1579
    /**
1580
     * Displays the PreRender component.
1581
     * If the component is not preRendered, it attempts to create and render it.
1582
     * Also, synchronizes the style of the PreRender component with the embedding
1583
     * element.
1584
     */
1585
    public async showPreRender(): Promise<TsEmbed> {
1586
        if (!this.viewConfig.preRenderId) {
14✔
1587
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1✔
1588
            return this;
1✔
1589
        }
1590
        if (!this.isPreRenderAvailable()) {
13✔
1591
            const isAvailable = this.connectPreRendered();
9✔
1592

1593
            if (!isAvailable) {
9✔
1594
                // if the Embed component is not preRendered , Render it now and
1595
                return this.preRender(true);
2✔
1596
            }
1597
            this.validatePreRenderViewConfig(this.viewConfig);
7✔
1598
            logger.debug('triggering UpdateEmbedParams', this.viewConfig);
7✔
1599
            this.executeAfterEmbedContainerLoaded(async () => {
7✔
1600
                try {
6✔
1601
                    const params = await this.getUpdateEmbedParamsObject();
6✔
1602
                    this.trigger(HostEvent.UpdateEmbedParams, params);
5✔
1603
                } catch (error) {
1604
                    logger.error(ERROR_MESSAGE.UPDATE_PARAMS_FAILED, error);
1✔
1605
                    this.handleError({
1✔
1606
                        errorType: ErrorDetailsTypes.API,
1607
                        message: error?.message || ERROR_MESSAGE.UPDATE_PARAMS_FAILED,
4!
1608
                        code: EmbedErrorCodes.UPDATE_PARAMS_FAILED,
1609
                        error: error?.message || error,
4!
1610
                    });
1611
                }
1612
            });
1613
        }
1614

1615
        this.beforePrerenderVisible();
11✔
1616

1617
        if (this.el) {
11✔
1618
            this.syncPreRenderStyle();
11✔
1619
            if (!this.viewConfig.doNotTrackPreRenderSize) {
11✔
1620
                this.resizeObserver = new ResizeObserver((entries) => {
11✔
1621
                    entries.forEach((entry) => {
1✔
1622
                        if (entry.contentRect && entry.target === this.el) {
1✔
1623
                            setStyleProperties(this.preRenderWrapper, {
1✔
1624
                                width: `${entry.contentRect.width}px`,
1625
                                height: `${entry.contentRect.height}px`,
1626
                            });
1627
                        }
1628
                    });
1629
                });
1630
                this.resizeObserver.observe(this.el);
11✔
1631
            }
1632
        }
1633

1634
        removeStyleProperties(this.preRenderWrapper, ['z-index', 'opacity', 'pointer-events', 'overflow']);
11✔
1635

1636
        this.subscribeToEvents();
11✔
1637

1638
        // Setup fullscreen change handler for prerendered components
1639
        if (this.iFrame) {
11✔
1640
            this.setupFullscreenChangeHandler();
11✔
1641
        }
1642

1643
        return this;
11✔
1644
    }
1645

1646
    /**
1647
     * Synchronizes the style properties of the PreRender component with the embedding
1648
     * element. This function adjusts the position, width, and height of the PreRender
1649
     * component
1650
     * to match the dimensions and position of the embedding element.
1651
     * @throws {Error} Throws an error if the embedding element (passed as domSelector)
1652
     * is not defined or not found.
1653
     */
1654
    public syncPreRenderStyle(): void {
1655
        if (!this.isPreRenderAvailable() || !this.el) {
12✔
1656
            logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1✔
1657
            return;
1✔
1658
        }
1659
        const elBoundingClient = this.el.getBoundingClientRect();
11✔
1660

1661
        setStyleProperties(this.preRenderWrapper, {
11✔
1662
            top: `${elBoundingClient.y + window.scrollY}px`,
1663
            left: `${elBoundingClient.x + window.scrollX}px`,
1664
            width: `${elBoundingClient.width}px`,
1665
            height: `${elBoundingClient.height}px`,
1666
        });
1667
    }
1668

1669
    /**
1670
     * Hides the PreRender component if it is available.
1671
     * If the component is not preRendered, it issues a warning.
1672
     */
1673
    public hidePreRender(): void {
1674
        if (!this.isPreRenderAvailable()) {
21✔
1675
            // if the embed component is not preRendered , nothing to hide
1676
            logger.warn('PreRender should be called before hiding it using hidePreRender.');
1✔
1677
            return;
1✔
1678
        }
1679
        const preRenderHideStyles = {
20✔
1680
            opacity: '0',
1681
            pointerEvents: 'none',
1682
            zIndex: '-1000',
1683
            position: 'absolute',
1684
            top: '0',
1685
            left: '0',
1686
            overflow: 'hidden',
1687
        };
1688
        setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
20✔
1689

1690
        if (this.resizeObserver) {
20✔
1691
            this.resizeObserver.disconnect();
2✔
1692
        }
1693

1694
        this.unsubscribeToEvents();
20✔
1695
    }
1696

1697
    /**
1698
     * Retrieves unique HTML element IDs for PreRender-related elements.
1699
     * These IDs are constructed based on the provided 'preRenderId' from 'viewConfig'.
1700
     * @returns {object} An object containing the IDs for the PreRender elements.
1701
     * @property {string} wrapper - The HTML element ID for the PreRender wrapper.
1702
     * @property {string} child - The HTML element ID for the PreRender child.
1703
     */
1704
    public getPreRenderIds() {
1705
        return {
81✔
1706
            wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
1707
            child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1708
        };
1709
    }
1710

1711
    /**
1712
     * Returns the answerService which can be used to make arbitrary graphql calls on top
1713
     * session.
1714
     * @param vizId [Optional] to get for a specific viz in case of a Liveboard.
1715
     * @version SDK: 1.25.0 / ThoughtSpot 9.10.0
1716
     */
1717
    public async getAnswerService(vizId?: string): Promise<AnswerService> {
1718
        const { session } = await this.trigger(HostEvent.GetAnswerSession, vizId ? { vizId } : {});
1!
1719
        return new AnswerService(session, null, this.embedConfig.thoughtSpotHost);
1✔
1720
    }
1721

1722
    /**
1723
     * Set up fullscreen change detection to automatically trigger ExitPresentMode
1724
     * when user exits fullscreen mode
1725
     */
1726
    private setupFullscreenChangeHandler() {
1727
        const embedConfig = getEmbedConfig();
32✔
1728
        const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
32!
1729

1730
        if (disableFullscreenPresentation) {
32✔
1731
            return;
29✔
1732
        }
1733

1734
        if (this.fullscreenChangeHandler) {
3!
UNCOV
1735
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
×
1736
        }
1737

1738
        this.fullscreenChangeHandler = () => {
3✔
1739
            const isFullscreen = !!document.fullscreenElement;
5✔
1740
            if (!isFullscreen) {
5✔
1741
                logger.info('Exited fullscreen mode - triggering ExitPresentMode');
2✔
1742
                // Only trigger if iframe is available and contentWindow is
1743
                // accessible
1744
                if (this.iFrame && this.iFrame.contentWindow) {
2✔
1745
                    this.trigger(HostEvent.ExitPresentMode);
1✔
1746
                } else {
1747
                    logger.debug('Skipping ExitPresentMode - iframe contentWindow not available');
1✔
1748
                }
1749
            }
1750
        };
1751

1752
        document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
3✔
1753
    }
1754

1755
    /**
1756
     * Remove fullscreen change handler
1757
     */
1758
    private removeFullscreenChangeHandler() {
1759
        if (this.fullscreenChangeHandler) {
32!
UNCOV
1760
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
×
1761
            this.fullscreenChangeHandler = null;
×
1762
        }
1763
    }
1764
}
1765

1766
/**
1767
 * Base class for embedding v1 experience
1768
 * Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
1769
 * which is currently under migration to v2
1770
 * @inheritdoc
1771
 */
1772
export class V1Embed extends TsEmbed {
14✔
1773
    protected viewConfig: ViewConfig;
1774

1775
    constructor(domSelector: DOMSelector, viewConfig: ViewConfig) {
1776
        super(domSelector, viewConfig);
326✔
1777
        this.viewConfig = { excludeRuntimeFiltersfromURL: false, ...viewConfig };
326✔
1778
    }
1779

1780
    /**
1781
     * Render the app in an iframe and set up event handlers
1782
     * @param iframeSrc
1783
     */
1784
    protected renderV1Embed(iframeSrc: string): Promise<any> {
1785
        return this.renderIFrame(iframeSrc);
296✔
1786
    }
1787

1788
    protected getRootIframeSrc(): string {
1789
        const queryParams = this.getEmbedParams();
305✔
1790
        let queryString = queryParams;
305✔
1791

1792
        if (!this.viewConfig.excludeRuntimeParametersfromURL) {
305✔
1793
            const runtimeParameters = this.viewConfig.runtimeParameters;
304✔
1794
            const parameterQuery = getRuntimeParameters(runtimeParameters || []);
304✔
1795
            queryString = [parameterQuery, queryParams].filter(Boolean).join('&');
304✔
1796
        }
1797

1798
        if (!this.viewConfig.excludeRuntimeFiltersfromURL) {
305✔
1799
            const runtimeFilters = this.viewConfig.runtimeFilters;
303✔
1800

1801
            const filterQuery = getFilterQuery(runtimeFilters || []);
303✔
1802
            queryString = [filterQuery, queryString].filter(Boolean).join('&');
303✔
1803
        }
1804
        return this.viewConfig.enableV2Shell_experimental
305✔
1805
            ? this.getEmbedBasePath(queryString)
1806
            : this.getV1EmbedBasePath(queryString);
1807
    }
1808

1809
    /**
1810
     * @inheritdoc
1811
     * @example
1812
     * ```js
1813
     * tsEmbed.on(EmbedEvent.Error, (data) => {
1814
     *   console.error(data);
1815
     * });
1816
     * ```
1817
     * @example
1818
     * ```js
1819
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1820
     *   console.log("Answer save clicked", data);
1821
     * }, {
1822
     *   start: true // This will trigger the callback on start of save
1823
     * });
1824
     * ```
1825
     */
1826
    public on(
1827
        messageType: EmbedEvent,
1828
        callback: MessageCallback,
1829
        options: MessageOptions = { start: false },
150✔
1830
    ): typeof TsEmbed.prototype {
1831
        const eventType = this.getCompatibleEventType(messageType);
1,745✔
1832
        return super.on(eventType, callback, options);
1,745✔
1833
    }
1834

1835
    /**
1836
     * Only for testing purposes.
1837
     * @hidden
1838
     */
1839

1840
    public test__executeCallbacks = this.executeCallbacks;
326✔
1841
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc