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

thoughtspot / visual-embed-sdk / #2815

20 Nov 2025 01:20PM UTC coverage: 94.403% (+0.1%) from 94.277%
#2815

Pull #329

shivam-kumar-ts
SCAL-256912 Refactor error handling to use EmbedErrorDetailsEvent interface
Pull Request #329: SCAL-256912 Enhance error handling by introducing EmbedErrorEvent interface

1350 of 1517 branches covered (88.99%)

Branch coverage included in aggregate %.

58 of 64 new or added lines in 5 files covered. (90.63%)

35 existing lines in 2 files now uncovered.

3204 of 3307 relevant lines covered (96.89%)

101.24 hits per line

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

91.03
/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
} from '../types';
63
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
64
import { processEventData, processAuthFailure } from '../utils/processData';
14✔
65
import pkgInfo from '../../package.json';
14✔
66
import {
14✔
67
    getAuthPromise, renderInQueue, handleAuth, notifyAuthFailure,
14✔
68
    getInitPromise,
69
    getIsInitCalled,
70
} from './base';
71
import { AuthFailureType } from '../auth';
72
import { getEmbedConfig } from './embedConfig';
14✔
73
import { ERROR_MESSAGE , ERROR_CODE} from '../errors';
14✔
74
import { getPreauthInfo } from '../utils/sessionInfoService';
14✔
75
import { HostEventClient } from './hostEventClient/host-event-client';
14✔
76
import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept';
14✔
77

14✔
78
const { version } = pkgInfo;
79

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

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

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

108
    /**
109
     * The DOM node where the ThoughtSpot app is to be embedded.
110
     */
111
    protected el: HTMLElement;
112

113
    /**
114
     * The key to store the embed instance in the DOM node
115
     */
116
    protected embedNodeKey = '__tsEmbed';
117

440✔
118
    protected isAppInitialized = false;
119

440✔
120
    /**
121
     * A reference to the iframe within which the ThoughtSpot app
122
     * will be rendered.
123
     */
124
    protected iFrame: HTMLIFrameElement;
125

126
    /**
127
     * Setter for the iframe element
128
     * @param {HTMLIFrameElement} iFrame HTMLIFrameElement
129
     */
130
    protected setIframeElement(iFrame: HTMLIFrameElement): void {
131
        this.iFrame = iFrame;
132
        this.hostEventClient.setIframeElement(iFrame);
403✔
133
    }
403✔
134

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

137
    protected embedConfig: EmbedConfig;
138

139
    /**
140
     * The ThoughtSpot hostname or IP address
141
     */
142
    protected thoughtSpotHost: string;
143

144
    /*
145
     * This is the base to access ThoughtSpot V2.
146
     */
147
    protected thoughtSpotV2Base: string;
148

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

156
    /**
157
     * A flag that is set to true post render.
158
     */
159
    protected isRendered: boolean;
160

161
    /**
162
     * A flag to mark if an error has occurred.
163
     */
164
    private isError: boolean;
165

166
    /**
167
     * A flag that is set to true post preRender.
168
     */
169
    private isPreRendered: boolean;
170

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

440✔
179
    private defaultHiddenActions = [Action.ReportError];
180

440✔
181
    private resizeObserver: ResizeObserver;
182

183
    protected hostEventClient: HostEventClient;
184

185
    protected isReadyForRenderPromise;
186

187
    /**
188
     * Handler for fullscreen change events
189
     */
190
    private fullscreenChangeHandler: (() => void) | null = null;
191

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

440✔
208
        this.hostEventClient = new HostEventClient(this.iFrame);
209
        this.isReadyForRenderPromise = getInitPromise().then(async () => {
440✔
210
            if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
440✔
211
                this.embedConfig.authTriggerContainer = domSelector;
440✔
212
            }
84✔
213
            this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
214
            this.thoughtSpotV2Base = getV2BasePath(embedConfig);
440✔
215
            this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
440✔
216
        });
440✔
217
    }
218

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

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

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

249
        return event.data?.type || event.data?.__type;
250
    }
3,074!
251

252
    /**
253
     * Extracts the port field from the event payload
254
     * @param event  The window message event
255
     * @returns
256
     */
257
    private getEventPort(event: MessageEvent) {
258
        if (event.ports.length && event.ports[0]) {
259
            return event.ports[0];
3,074✔
260
        }
2,365✔
261
        return null;
262
    }
709✔
263

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

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

410✔
293
        // Check if this is a FullAppEmbed (AppEmbed)
294
        // showPrimaryNavbar defaults to true if not explicitly set to false
295
        return (
296
            appViewConfig.embedComponentType === 'AppEmbed'
410✔
297
            && appViewConfig.showPrimaryNavbar === true
548✔
298
        );
299
    }
300

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

319
    private subscribedListeners: Record<string, any> = {};
320

440✔
321
    /**
322
     * Subscribe to network events (online/offline) that should
323
     * work regardless of auth status
324
     */
325
    private subscribeToNetworkEvents() {
326
        this.unsubscribeToNetworkEvents();
327

398✔
328
        const onlineEventListener = (e: Event) => {
329
            this.trigger(HostEvent.Reload);
398✔
330
        };
265✔
331
        window.addEventListener('online', onlineEventListener);
332

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

398✔
345
        this.subscribedListeners.online = onlineEventListener;
346
        this.subscribedListeners.offline = offlineEventListener;
398✔
347
    }
398✔
348

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

360
    private messageEventListener = (event: MessageEvent<any>) => {
361
        const eventType = this.getEventType(event);
440✔
362
        const eventPort = this.getEventPort(event);
3,074✔
363
        const eventData = this.formatEventData(event, eventType);
3,074✔
364
        if (event.source === this.iFrame.contentWindow) {
3,074✔
365
            const processedEventData = processEventData(
3,074✔
366
                eventType,
71✔
367
                eventData,
368
                this.thoughtSpotHost,
369
                this.isPreRendered ? this.preRenderWrapper : this.el,
370
            );
71✔
371

372
            if (eventType === EmbedEvent.ApiIntercept) {
373
                this.handleApiInterceptEvent({ eventData, eventPort });
71✔
374
                return;
10✔
375
            }
10✔
376

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

391✔
390
        window.addEventListener('message', this.messageEventListener);
391

391✔
392
        this.subscribedListeners.message = this.messageEventListener;
393
    }
391✔
394
   
395

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

409

410
    private unsubscribeToNetworkEvents() {
411
        if (this.subscribedListeners.online) {
412
            window.removeEventListener('online', this.subscribedListeners.online);
398✔
413
            delete this.subscribedListeners.online;
4✔
414
        }
4✔
415
        if (this.subscribedListeners.offline) {
416
            window.removeEventListener('offline', this.subscribedListeners.offline);
398✔
417
            delete this.subscribedListeners.offline;
4✔
418
        }
4✔
419
    }
420

421
    private unsubscribeToMessageEvents() {
422
        if (this.subscribedListeners.message) {
423
            window.removeEventListener('message', this.subscribedListeners.message);
392✔
424
            delete this.subscribedListeners.message;
5✔
425
        }
5✔
426
    }
427

428
    private unsubscribeToEvents() {
429
        Object.keys(this.subscribedListeners).forEach((key) => {
430
            window.removeEventListener(key, this.subscribedListeners[key]);
43✔
431
        });
107✔
432
    }
433

434
    protected async getAuthTokenForCookielessInit() {
435
        let authToken = '';
436
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
23✔
437

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

445
        return authToken;
446
    }
4✔
447

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

484
        return baseInitData;
485
    }
20✔
486

487
    protected async getAppInitData() {
488
        return this.getDefaultAppInitData();
489
    }
22✔
490

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

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

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

561
    /**
562
     * Register APP_INIT event and sendback init payload
563
     */
564
    private registerAppInit = () => {
565
        this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
440✔
566
        this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
440✔
567
        this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
440✔
568

440✔
569
        const embedListenerReadyHandler = this.createEmbedContainerHandler(EmbedEvent.EmbedListenerReady);
570
        this.on(EmbedEvent.EmbedListenerReady, embedListenerReadyHandler, { start: false }, true);
440✔
571

440✔
572
        const authInitHandler = this.createEmbedContainerHandler(EmbedEvent.AuthInit);
573
        this.on(EmbedEvent.AuthInit, authInitHandler, { start: false }, true);
440✔
574
    };
440✔
575

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

591
        return `${basePath}#`;
592
    }
128✔
593

594
    protected getUpdateEmbedParamsObject() {
595
        let queryParams = this.getEmbedParamsObject();
596
        queryParams = { ...this.viewConfig, ...queryParams, ...this.getAppInitData() };
2✔
597
        return queryParams;
2✔
598
    }
2✔
599

600
    /**
601
     * Common query params set for all the embed modes.
602
     * @param queryParams
603
     * @returns queryParams
604
     */
605
    protected getBaseQueryParams(queryParams: Record<any, any> = {}) {
606
        let hostAppUrl = window?.location?.host || '';
143✔
607

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

644
        const {
645
            disabledActions,
646
            disabledActionReason,
647
            hiddenActions,
648
            visibleActions,
649
            hiddenTabs,
650
            visibleTabs,
651
            showAlerts,
652
            additionalFlags: additionalFlagsFromView,
653
            locale,
654
            customizations,
655
            contextMenuTrigger,
656
            linkOverride,
657
            insertInToSlide,
658
            disableRedirectionLinksInNewTab,
659
            overrideOrgId,
660
            exposeTranslationIDs,
661
            primaryAction,
662
        } = this.viewConfig;
663

403✔
664
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
665

403✔
666
        const additionalFlags = {
667
            ...additionalFlagsFromInit,
403✔
668
            ...additionalFlagsFromView,
669
        };
670

671
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
672
            this.handleError({
403✔
673
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
4✔
674
                message: ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
675
                code: ERROR_CODE.CONFLICTING_ACTIONS_CONFIG,
676
                error : ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
677
            });
678
            return queryParams;
679
        }
4✔
680

681
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
682
            this.handleError({
399✔
683
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
4✔
684
                message: ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
685
                code: ERROR_CODE.CONFLICTING_TABS_CONFIG,
686
                error : ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
687
            });
688
            return queryParams;
689
        }
4✔
690
        if (primaryAction) {
691
            queryParams[Param.PrimaryAction] = primaryAction;
395!
UNCOV
692
        }
×
693

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

726
        const embedCustomizations = this.embedConfig.customizations;
727
        const spriteUrl = customizations?.iconSpriteUrl || embedCustomizations?.iconSpriteUrl;
395✔
728
        if (spriteUrl) {
395✔
729
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
395✔
730
        }
1✔
731

732
        const stringIDsUrl = customizations?.content?.stringIDsUrl
733
            || embedCustomizations?.content?.stringIDsUrl;
395✔
734
        if (stringIDsUrl) {
2,370✔
735
            queryParams[Param.StringIDsUrl] = stringIDsUrl;
395✔
736
        }
2✔
737

738
        if (showAlerts !== undefined) {
739
            queryParams[Param.ShowAlerts] = showAlerts;
395✔
740
        }
1✔
741
        if (locale !== undefined) {
742
            queryParams[Param.Locale] = locale;
395✔
743
        }
1✔
744

745
        if (linkOverride) {
746
            queryParams[Param.LinkOverride] = linkOverride;
395!
UNCOV
747
        }
×
748
        if (insertInToSlide) {
749
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
395!
UNCOV
750
        }
×
751
        if (disableRedirectionLinksInNewTab) {
752
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
395!
UNCOV
753
        }
×
754
        if (overrideOrgId !== undefined) {
755
            queryParams[Param.OverrideOrgId] = overrideOrgId;
395✔
756
        }
3✔
757

758
        if (this.isPreAuthCacheEnabled()) {
759
            queryParams[Param.preAuthCache] = true;
395✔
760
        }
386✔
761

762
        queryParams[Param.OverrideNativeConsole] = true;
763
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
395✔
764

395✔
765
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
766
            Object.assign(queryParams, additionalFlags);
395✔
767
        }
8✔
768

769
        // Do not add any flags below this, as we want additional flags to
770
        // override other flags
771

772
        return queryParams;
773
    }
395✔
774

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

791
    protected getEmbedParams() {
792
        const queryParams = this.getEmbedParamsObject();
793
        return getQueryParamString(queryParams);
×
UNCOV
794
    }
×
795

796
    protected getEmbedParamsObject() {
797
        const params = this.getBaseQueryParams();
798
        return params;
×
UNCOV
799
    }
×
800

801
    protected getRootIframeSrc() {
802
        const query = this.getEmbedParams();
803
        return this.getEmbedBasePath(query);
103✔
804
    }
103✔
805

806
    protected createIframeEl(frameSrc: string): HTMLIFrameElement {
807
        const iFrame = document.createElement('iframe');
808

376✔
809
        iFrame.src = frameSrc;
810
        iFrame.id = TS_EMBED_ID;
376✔
811
        iFrame.setAttribute('data-ts-iframe', 'true');
376✔
812

376✔
813
        // according to screenfull.js documentation
814
        // allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be
815
        // true
816
        iFrame.allowFullscreen = true;
817
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
376✔
818
        // @ts-ignore
819
        iFrame.webkitallowfullscreen = true;
820
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
376✔
821
        // @ts-ignore
822
        iFrame.mozallowfullscreen = true;
823
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
376✔
824
        // @ts-ignore
825
        iFrame.allow = 'clipboard-read; clipboard-write; fullscreen;';
826

376✔
827
        const frameParams = this.viewConfig.frameParams;
828
        const { height: frameHeight, width: frameWidth, ...restParams } = frameParams || {};
376✔
829
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
376✔
830
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
376✔
831
        setAttributes(iFrame, restParams);
376✔
832

376✔
833
        iFrame.style.width = `${width}`;
834
        iFrame.style.height = `${height}`;
376✔
835
        // Set minimum height to the frame so that,
376✔
836
        // scaling down on the fullheight doesn't make it too small.
837
        iFrame.style.minHeight = `${height}`;
838
        iFrame.style.border = '0';
376✔
839
        iFrame.name = 'ThoughtSpot Embedded Analytics';
376✔
840
        return iFrame;
376✔
841
    }
376✔
842

843
    protected handleInsertionIntoDOM(child: string | Node): void {
844
        if (this.isPreRendered) {
845
            this.insertIntoDOMForPreRender(child);
392✔
846
        } else {
14✔
847
            this.insertIntoDOM(child);
848
        }
378✔
849
        if (this.insertedDomEl instanceof Node) {
850
            (this.insertedDomEl as any)[this.embedNodeKey] = this;
392✔
851
        }
387✔
852
    }
853

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

870
        return renderInQueue((nextInQueue) => {
871
            const initTimestamp = Date.now();
392✔
872

392✔
873
            this.executeCallbacks(EmbedEvent.Init, {
874
                data: {
392✔
875
                    timestamp: initTimestamp,
876
                },
877
                type: EmbedEvent.Init,
878
            });
879

880
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
881

392✔
882
            // Always subscribe to network events, regardless of auth status
883
            this.subscribeToNetworkEvents();
884

392✔
885
            return getAuthPromise()
886
                ?.then((isLoggedIn: boolean) => {
392!
887
                    if (!isLoggedIn) {
888
                        this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
388✔
889
                        return;
3✔
890
                    }
3✔
891

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

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

950
    protected createPreRenderWrapper(): HTMLDivElement {
951
        const preRenderIds = this.getPreRenderIds();
952

14✔
953
        document.getElementById(preRenderIds.wrapper)?.remove();
954

14✔
955
        const preRenderWrapper = document.createElement('div');
956
        preRenderWrapper.id = preRenderIds.wrapper;
14✔
957
        const initialPreRenderWrapperStyle = {
14✔
958
            position: 'absolute',
14✔
959
            width: '100vw',
960
            height: '100vh',
961
        };
962
        setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
963

14✔
964
        return preRenderWrapper;
965
    }
14✔
966

967
    protected preRenderWrapper: HTMLElement;
968

969
    protected preRenderChild: HTMLElement;
970

971
    protected connectPreRendered(): boolean {
972
        const preRenderIds = this.getPreRenderIds();
973
        const preRenderWrapperElement = document.getElementById(preRenderIds.wrapper);
19✔
974
        this.preRenderWrapper = this.preRenderWrapper || preRenderWrapperElement;
19✔
975

19✔
976
        this.preRenderChild = this.preRenderChild || document.getElementById(preRenderIds.child);
977

19✔
978
        if (this.preRenderWrapper && this.preRenderChild) {
979
            this.isPreRendered = true;
19✔
980
            if (this.preRenderChild instanceof HTMLIFrameElement) {
5✔
981
                this.setIframeElement(this.preRenderChild);
5✔
982
            }
5✔
983
            this.insertedDomEl = this.preRenderWrapper;
984
            this.isRendered = true;
5✔
985
        }
5✔
986

987
        return this.isPreRenderAvailable();
988
    }
19✔
989

990
    protected isPreRenderAvailable(): boolean {
991
        return (
992
            this.isRendered
48✔
993
            && this.isPreRendered
102✔
994
            && Boolean(this.preRenderWrapper && this.preRenderChild)
995
        );
54✔
996
    }
997

998
    protected createPreRenderChild(child: string | Node): HTMLElement {
999
        const preRenderIds = this.getPreRenderIds();
1000

14✔
1001
        document.getElementById(preRenderIds.child)?.remove();
1002

14✔
1003
        if (child instanceof HTMLElement) {
1004
            child.id = preRenderIds.child;
14✔
1005
            return child;
13✔
1006
        }
13✔
1007

1008
        const divChildNode = document.createElement('div');
1009
        setStyleProperties(divChildNode, { width: '100%', height: '100%' });
1✔
1010
        divChildNode.id = preRenderIds.child;
1✔
1011

1✔
1012
        if (typeof child === 'string') {
1013
            divChildNode.innerHTML = child;
1!
1014
        } else {
1✔
1015
            divChildNode.appendChild(child);
UNCOV
1016
        }
×
1017

1018
        return divChildNode;
1019
    }
1✔
1020

1021
    protected insertIntoDOMForPreRender(child: string | Node): void {
1022
        const preRenderChild = this.createPreRenderChild(child);
1023
        const preRenderWrapper = this.createPreRenderWrapper();
14✔
1024
        preRenderWrapper.appendChild(preRenderChild);
14✔
1025

14✔
1026
        this.preRenderChild = preRenderChild;
1027
        this.preRenderWrapper = preRenderWrapper;
14✔
1028

14✔
1029
        if (preRenderChild instanceof HTMLIFrameElement) {
1030
            this.setIframeElement(preRenderChild);
14✔
1031
        }
13✔
1032
        this.insertedDomEl = preRenderWrapper;
1033

14✔
1034
        if (this.showPreRenderByDefault) {
1035
            this.showPreRender();
14✔
1036
        } else {
2✔
1037
            this.hidePreRender();
1038
        }
12✔
1039

1040
        document.body.appendChild(preRenderWrapper);
1041
    }
14✔
1042

1043
    private showPreRenderByDefault = false;
1044

440✔
1045
    protected insertIntoDOM(child: string | Node): void {
1046
        if (this.viewConfig.insertAsSibling) {
1047
            if (typeof child === 'string') {
378✔
1048
                const div = document.createElement('div');
12✔
1049
                div.innerHTML = child;
1✔
1050
                div.id = TS_EMBED_ID;
1✔
1051

1✔
1052
                child = div;
1053
            }
1✔
1054
            if (this.el.nextElementSibling?.id === TS_EMBED_ID) {
1055
                this.el.nextElementSibling.remove();
12✔
1056
            }
1✔
1057
            this.el.parentElement.insertBefore(child, this.el.nextSibling);
1058
            this.insertedDomEl = child;
12✔
1059
        } else if (typeof child === 'string') {
12✔
1060
            this.el.innerHTML = child;
366✔
1061
            this.insertedDomEl = this.el.children[0];
5✔
1062
        } else {
5✔
1063
            this.el.innerHTML = '';
1064
            this.el.appendChild(child);
361✔
1065
            this.insertedDomEl = child;
361✔
1066
        }
361✔
1067
    }
1068

1069
    /**
1070
     * Sets the height of the iframe
1071
     * @param height The height in pixels
1072
     */
1073
    protected setIFrameHeight(height: number | string): void {
1074
        this.iFrame.style.height = getCssDimension(height);
1075
    }
3✔
1076

1077
    /**
1078
     * We can process the customer given payload before sending it to the embed port
1079
     * Embed event handler -> responder -> createEmbedEventResponder -> send response
1080
     * @param eventPort The event port for a specific MessageChannel
1081
     * @param eventType The event type
1082
     * @returns 
1083
     */
1084
    protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => {
1085

440✔
1086
        const getPayloadToSend = (payload: any) => {
1087
            if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept) {
62✔
1088
                return processLegacyInterceptResponse(payload);
28✔
1089
            }
1✔
1090
            if (eventType === EmbedEvent.ApiIntercept) {
1091
                return processApiInterceptResponse(payload);
27!
UNCOV
1092
            }
×
1093
            return payload;
1094
        }   
27✔
1095
        return (payload: any) => {
1096
            const payloadToSend = getPayloadToSend(payload);
62✔
1097
            this.triggerEventOnPort(eventPort, payloadToSend);
28✔
1098
        }
28✔
1099
    }
1100

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

1131
    /**
1132
     * Returns the ThoughtSpot hostname or IP address.
1133
     */
1134
    protected getThoughtSpotHost(): string {
1135
        return this.thoughtSpotHost;
UNCOV
1136
    }
×
1137

1138
    /**
1139
     * Gets the v1 event type (if applicable) for the EmbedEvent type
1140
     * @param eventType The v2 event type
1141
     * @returns The corresponding v1 event type if one exists
1142
     * or else the v2 event type itself
1143
     */
1144
    protected getCompatibleEventType(eventType: EmbedEvent): EmbedEvent {
1145
        return V1EventMap[eventType] || eventType;
1146
    }
1,541✔
1147

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

1165
        if (iframeScrolled < 0) {
1166
            iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
5!
1167
            iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
×
1168
            iframeOffset = 0;
×
UNCOV
1169
        } else {
×
1170
            iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
1171
            iframeOffset = iframeScrolled;
5✔
1172
        }
5✔
1173
        const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
1174
        return {
5✔
1175
            iframeCenter,
5✔
1176
            iframeScrolled,
1177
            iframeHeight,
1178
            viewPortHeight,
1179
            iframeVisibleViewPort,
1180
        };
1181
    }
1182

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

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

1244
    /**
1245
     * Triggers an event on specific Port registered against
1246
     * for the EmbedEvent
1247
     * @param eventType The message type
1248
     * @param data The payload to send
1249
     * @param eventPort
1250
     * @param payload
1251
     */
1252
    private triggerEventOnPort(eventPort: MessagePort | void, payload: any) {
1253
        if (eventPort) {
1254
            try {
28✔
1255
                eventPort.postMessage({
24✔
1256
                    type: payload.type,
24✔
1257
                    data: payload.data,
1258
                });
1259
            } catch (e) {
1260
                eventPort.postMessage({ error: e });
1261
                logger.log(e);
×
UNCOV
1262
            }
×
1263
        } else {
1264
            logger.log('Event Port is not defined');
1265
        }
4✔
1266
    }
1267

1268
    /**
1269
    * @hidden
1270
    * Internal state to track if the embed container is loaded.
1271
    * This is used to trigger events after the embed container is loaded.
1272
    */
1273
    public isEmbedContainerLoaded = false;
1274

440✔
1275
    /**
1276
     * @hidden
1277
     * Internal state to track the callbacks to be executed after the embed container 
1278
     * is loaded.
1279
     * This is used to trigger events after the embed container is loaded.
1280
     */
1281
    private embedContainerReadyCallbacks: Array<() => void> = [];
1282

440✔
1283
    protected getPreRenderObj<T extends TsEmbed>(): T {
1284
        const embedObj = (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
1285
        if (embedObj === (this as any)) {
30✔
1286
            logger.info('embedObj is same as this');
30✔
1287
        }
5✔
1288
        return embedObj;
1289
    }
30✔
1290

1291
    private checkEmbedContainerLoaded() {
1292
        if (this.isEmbedContainerLoaded) return true;
1293

28✔
1294
        const preRenderObj = this.getPreRenderObj<TsEmbed>();
1295
        if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
22✔
1296
            this.isEmbedContainerLoaded = true;
22✔
1297
        }
2✔
1298

1299
        return this.isEmbedContainerLoaded;
1300
    }
22✔
1301
    private executeEmbedContainerReadyCallbacks() {
1302
        logger.debug('executePendingEvents', this.embedContainerReadyCallbacks);
1303
        this.embedContainerReadyCallbacks.forEach((callback) => {
14✔
1304
            callback?.();
14✔
1305
        });
12!
1306
        this.embedContainerReadyCallbacks = [];
1307
    }
14✔
1308

1309
    /**
1310
     * Executes a callback after the embed container is loaded.
1311
     * @param callback The callback to execute
1312
     */
1313
    protected executeAfterEmbedContainerLoaded(callback: () => void) {
1314
        if (this.checkEmbedContainerLoaded()) {
1315
            callback?.();
27✔
1316
        } else {
7!
1317
            logger.debug('pushing callback to embedContainerReadyCallbacks', callback);
1318
            this.embedContainerReadyCallbacks.push(callback);
20✔
1319
        }
20✔
1320
    }
1321

1322
    protected createEmbedContainerHandler = (source: EmbedEvent.AuthInit | EmbedEvent.EmbedListenerReady) => () => {
1323
        const processEmbedContainerReady = () => {
882✔
1324
            logger.debug('processEmbedContainerReady');
10✔
1325
            this.isEmbedContainerLoaded = true;
9✔
1326
            this.executeEmbedContainerReadyCallbacks();
9✔
1327
        }
9✔
1328
        if (source === EmbedEvent.AuthInit) {
1329
            const AUTH_INIT_FALLBACK_DELAY = 1000;
10✔
1330
            // Wait for 1 second to ensure the embed container is loaded
7✔
1331
            // This is a workaround to ensure the embed container is loaded
1332
            // this is needed until all clusters have EmbedListenerReady event
1333
            setTimeout(processEmbedContainerReady, AUTH_INIT_FALLBACK_DELAY);
1334
        } else if (source === EmbedEvent.EmbedListenerReady) {
7✔
1335
            processEmbedContainerReady();
3✔
1336
        }
3✔
1337
    }
1338

1339
    /**
1340
     * Triggers an event to the embedded app
1341
     * @param {HostEvent} messageType The event type
1342
     * @param {any} data The payload to send with the message
1343
     * @returns A promise that resolves with the response from the embedded app
1344
     */
1345
    public async trigger<HostEventT extends HostEvent, PayloadT>(
1346
        messageType: HostEventT,
1347
        data: TriggerPayload<PayloadT, HostEventT> = {} as any,
1348
    ): Promise<TriggerResponse<PayloadT, HostEventT>> {
219✔
1349
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
1350

315✔
1351
        if (!this.isRendered) {
1352
            this.handleError({
315!
NEW
1353
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
×
1354
                message: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1355
                code: ERROR_CODE.RENDER_NOT_CALLED,
1356
                error: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1357
            });
1358
            return null;
UNCOV
1359
        }
×
1360

1361
        if (!messageType) {
1362
            this.handleError({
315!
NEW
1363
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
×
1364
                message: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1365
                code: ERROR_CODE.HOST_EVENT_TYPE_UNDEFINED,
1366
                error: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1367
            });
1368
            return null;
UNCOV
1369
        }
×
1370

1371
        // Check if iframe exists before triggering - 
1372
        // this prevents the error when auth fails
1373
        if (!this.iFrame) {
1374
            logger.debug(
315✔
1375
                `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
24✔
1376
            );
1377
            return null;
1378
        }
24✔
1379

1380
        // send an empty object, this is needed for liveboard default handlers
1381
        return this.hostEventClient.triggerHostEvent(messageType, data);
1382
    }
291✔
1383

1384
    /**
1385
     * Triggers an event to the embedded app, skipping the UI flow.
1386
     * @param {UIPassthroughEvent} apiName - The name of the API to be triggered.
1387
     * @param {UIPassthroughRequest} parameters - The parameters to be passed to the API.
1388
     * @returns {Promise<UIPassthroughRequest>} - A promise that resolves with the response
1389
     * from the embedded app.
1390
     */
1391
    public async triggerUIPassThrough<UIPassthroughEventT extends UIPassthroughEvent>(
1392
        apiName: UIPassthroughEventT,
1393
        parameters: UIPassthroughRequest<UIPassthroughEventT>,
1394
    ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> {
1395
        const response = this.hostEventClient.triggerUIPassthroughApi(apiName, parameters);
1396
        return response;
3✔
1397
    }
3✔
1398

1399
    /**
1400
     * Marks the ThoughtSpot object to have been rendered
1401
     * Needs to be overridden by subclasses to do the actual
1402
     * rendering of the iframe.
1403
     * @param args
1404
     */
1405
    public async render(): Promise<TsEmbed> {
1406
        if (!getIsInitCalled()) {
1407
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
394✔
1408
        }
2✔
1409
        await this.isReadyForRenderPromise;
1410
        this.isRendered = true;
394✔
1411

394✔
1412
        return this;
1413
    }
394✔
1414

1415
    public getIframeSrc(): string {
1416
        return '';
1417
    }
1✔
1418

1419
    protected handleRenderForPrerender() {
1420
        return this.render();
1421
    }
9✔
1422

1423
    /**
1424
     * Creates the preRender shell
1425
     * @param showPreRenderByDefault - Show the preRender after render, hidden by default
1426
     */
1427

1428
    public async preRender(showPreRenderByDefault = false, replaceExistingPreRender = false): Promise<TsEmbed> {
1429
        if (!this.viewConfig.preRenderId) {
24✔
1430
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
15✔
1431
            return this;
1✔
1432
        }
1✔
1433
        this.isPreRendered = true;
1434
        this.showPreRenderByDefault = showPreRenderByDefault;
14✔
1435

14✔
1436
        const isAlreadyRendered = this.connectPreRendered();
1437
        if (isAlreadyRendered && !replaceExistingPreRender) {
14✔
1438
            return this;
14!
UNCOV
1439
        }
×
1440

1441
        return this.handleRenderForPrerender();
1442
    }
14✔
1443

1444
    /**
1445
     * Get the Post Url Params for THOUGHTSPOT from the current
1446
     * host app URL.
1447
     * THOUGHTSPOT URL params starts with a prefix "ts-"
1448
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
1449
     */
1450
    public getThoughtSpotPostUrlParams(
1451
        additionalParams: { [key: string]: string | number } = {},
1452
    ): string {
378✔
1453
        const urlHash = window.location.hash;
1454
        const queryParams = window.location.search;
399✔
1455
        const postHashParams = urlHash.split('?');
399✔
1456
        const postURLParams = postHashParams[postHashParams.length - 1];
399✔
1457
        const queryParamsObj = new URLSearchParams(queryParams);
399✔
1458
        const postURLParamsObj = new URLSearchParams(postURLParams);
399✔
1459
        const params = new URLSearchParams();
399✔
1460

399✔
1461
        const addKeyValuePairCb = (value: string, key: string): void => {
1462
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
399✔
1463
                params.append(key, value);
8✔
1464
            }
5✔
1465
        };
1466
        queryParamsObj.forEach(addKeyValuePairCb);
1467
        postURLParamsObj.forEach(addKeyValuePairCb);
399✔
1468
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
399✔
1469

399✔
1470
        let tsParams = params.toString();
1471
        tsParams = tsParams ? `?${tsParams}` : '';
399✔
1472

399✔
1473
        return tsParams;
1474
    }
399✔
1475

1476
    /**
1477
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
1478
     * @version SDK: 1.19.1 | ThoughtSpot: *
1479
     */
1480
    public destroy(): void {
1481
        try {
1482
            this.removeFullscreenChangeHandler();
30✔
1483
            this.unsubscribeToEvents();
30✔
1484
            if (!getEmbedConfig().waitForCleanupOnDestroy) {
30✔
1485
                this.trigger(HostEvent.DestroyEmbed)
30✔
1486
                this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
27✔
1487
            } else {
27✔
1488
                const cleanupTimeout = getEmbedConfig().cleanupTimeout;
1489
                Promise.race([
3✔
1490
                    this.trigger(HostEvent.DestroyEmbed),
3✔
1491
                    new Promise((resolve) => setTimeout(resolve, cleanupTimeout)),
1492
                ]).then(() => {
3✔
1493
                    this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
1494
                }).catch((e) => {
3!
1495
                    logger.log('Error destroying TS Embed', e);
UNCOV
1496
                });
×
1497
            }
1498
        } catch (e) {
1499
            logger.log('Error destroying TS Embed', e);
UNCOV
1500
        }
×
1501
    }
1502

1503
    public getUnderlyingFrameElement(): HTMLIFrameElement {
1504
        return this.iFrame;
1505
    }
1✔
1506

1507
    /**
1508
     * Prerenders a generic instance of the TS component.
1509
     * This means without the path but with the flags already applied.
1510
     * This is useful for prerendering the component in the background.
1511
     * @version SDK: 1.22.0
1512
     * @returns
1513
     */
1514
    public async prerenderGeneric(): Promise<any> {
1515
        if (!getIsInitCalled()) {
1516
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
8✔
1517
        }
1✔
1518
        await this.isReadyForRenderPromise;
1519

8✔
1520
        const prerenderFrameSrc = this.getRootIframeSrc();
1521
        this.isRendered = true;
8✔
1522
        return this.renderIFrame(prerenderFrameSrc);
8✔
1523
    }
8✔
1524

1525
    protected beforePrerenderVisible(): void {
1526
        // Override in subclass
1527
    }
1528

1529
    private validatePreRenderViewConfig = (viewConfig: ViewConfig) => {
1530
        const preRenderAllowedKeys = ['preRenderId', 'vizId', 'liveboardId'];
440✔
1531
        const preRenderedObject = (this.insertedDomEl as any)?.[this.embedNodeKey] as TsEmbed;
3✔
1532
        if (!preRenderedObject) return;
3!
1533
        if (viewConfig.preRenderId) {
3!
1534
            const allOtherKeys = Object.keys(viewConfig).filter(
3✔
1535
                (key) => !preRenderAllowedKeys.includes(key) && !key.startsWith('on'),
3✔
1536
            );
18✔
1537

1538
            allOtherKeys.forEach((key: keyof ViewConfig) => {
1539
                if (
3✔
1540
                    !isUndefined(viewConfig[key])
10✔
1541
                    && !isEqual(viewConfig[key], preRenderedObject.viewConfig[key])
20✔
1542
                ) {
1543
                    logger.warn(
1544
                        `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
4✔
1545
                        + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
4!
1546
                        + `but a different value "${JSON.stringify(viewConfig[key])}" `
1547
                        + 'was passed to the Embed component. '
1548
                        + 'The new value provided is ignored, the value provided during '
1549
                        + 'preRender is used.',
1550
                    );
1551
                }
1552
            });
1553
        }
1554
    };
1555

1556
    /**
1557
     * Displays the PreRender component.
1558
     * If the component is not preRendered, it attempts to create and render it.
1559
     * Also, synchronizes the style of the PreRender component with the embedding
1560
     * element.
1561
     */
1562
    public async showPreRender(): Promise<TsEmbed> {
1563
        if (!this.viewConfig.preRenderId) {
1564
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
9✔
1565
            return this;
1✔
1566
        }
1✔
1567
        if (!this.isPreRenderAvailable()) {
1568
            const isAvailable = this.connectPreRendered();
8✔
1569

5✔
1570
            if (!isAvailable) {
1571
                // if the Embed component is not preRendered , Render it now and
5✔
1572
                return this.preRender(true);
1573
            }
2✔
1574
            this.validatePreRenderViewConfig(this.viewConfig);
1575
            logger.debug('triggering UpdateEmbedParams', this.viewConfig);
3✔
1576
            this.executeAfterEmbedContainerLoaded(() => {
3✔
1577
                this.trigger(HostEvent.UpdateEmbedParams, this.getUpdateEmbedParamsObject());
3✔
1578
            });
2✔
1579
        }
1580

1581
        this.beforePrerenderVisible();
1582

6✔
1583
        if (this.el) {
1584
            this.syncPreRenderStyle();
6✔
1585
            if (!this.viewConfig.doNotTrackPreRenderSize) {
6✔
1586
                this.resizeObserver = new ResizeObserver((entries) => {
6✔
1587
                    entries.forEach((entry) => {
6✔
1588
                        if (entry.contentRect && entry.target === this.el) {
1✔
1589
                            setStyleProperties(this.preRenderWrapper, {
1✔
1590
                                width: `${entry.contentRect.width}px`,
1✔
1591
                                height: `${entry.contentRect.height}px`,
1592
                            });
1593
                        }
1594
                    });
1595
                });
1596
                this.resizeObserver.observe(this.el);
1597
            }
6✔
1598
        }
1599

1600
        removeStyleProperties(this.preRenderWrapper, ['z-index', 'opacity', 'pointer-events']);
1601

6✔
1602
        this.subscribeToEvents();
1603

6✔
1604
        // Setup fullscreen change handler for prerendered components
1605
        if (this.iFrame) {
1606
            this.setupFullscreenChangeHandler();
6✔
1607
        }
6✔
1608

1609
        return this;
1610
    }
6✔
1611

1612
    /**
1613
     * Synchronizes the style properties of the PreRender component with the embedding
1614
     * element. This function adjusts the position, width, and height of the PreRender
1615
     * component
1616
     * to match the dimensions and position of the embedding element.
1617
     * @throws {Error} Throws an error if the embedding element (passed as domSelector)
1618
     * is not defined or not found.
1619
     */
1620
    public syncPreRenderStyle(): void {
1621
        if (!this.isPreRenderAvailable() || !this.el) {
1622
            logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
7✔
1623
            return;
1✔
1624
        }
1✔
1625
        const elBoundingClient = this.el.getBoundingClientRect();
1626

6✔
1627
        setStyleProperties(this.preRenderWrapper, {
1628
            top: `${elBoundingClient.y + window.scrollY}px`,
6✔
1629
            left: `${elBoundingClient.x + window.scrollX}px`,
1630
            width: `${elBoundingClient.width}px`,
1631
            height: `${elBoundingClient.height}px`,
1632
        });
1633
    }
1634

1635
    /**
1636
     * Hides the PreRender component if it is available.
1637
     * If the component is not preRendered, it issues a warning.
1638
     */
1639
    public hidePreRender(): void {
1640
        if (!this.isPreRenderAvailable()) {
1641
            // if the embed component is not preRendered , nothing to hide
14✔
1642
            logger.warn('PreRender should be called before hiding it using hidePreRender.');
1643
            return;
1✔
1644
        }
1✔
1645
        const preRenderHideStyles = {
1646
            opacity: '0',
13✔
1647
            pointerEvents: 'none',
1648
            zIndex: '-1000',
1649
            position: 'absolute ',
1650
        };
1651
        setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
1652

13✔
1653
        if (this.resizeObserver) {
1654
            this.resizeObserver.disconnect();
13✔
1655
        }
1✔
1656

1657
        this.unsubscribeToEvents();
1658
    }
13✔
1659

1660
    /**
1661
     * Retrieves unique HTML element IDs for PreRender-related elements.
1662
     * These IDs are constructed based on the provided 'preRenderId' from 'viewConfig'.
1663
     * @returns {object} An object containing the IDs for the PreRender elements.
1664
     * @property {string} wrapper - The HTML element ID for the PreRender wrapper.
1665
     * @property {string} child - The HTML element ID for the PreRender child.
1666
     */
1667
    public getPreRenderIds() {
1668
        return {
1669
            wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
57✔
1670
            child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1671
        };
1672
    }
1673

1674
    /**
1675
     * Returns the answerService which can be used to make arbitrary graphql calls on top
1676
     * session.
1677
     * @param vizId [Optional] to get for a specific viz in case of a Liveboard.
1678
     * @version SDK: 1.25.0 / ThoughtSpot 9.10.0
1679
     */
1680
    public async getAnswerService(vizId?: string): Promise<AnswerService> {
1681
        const { session } = await this.trigger(HostEvent.GetAnswerSession, vizId ? { vizId } : {});
1682
        return new AnswerService(session, null, this.embedConfig.thoughtSpotHost);
1!
1683
    }
1✔
1684

1685
    /**
1686
     * Set up fullscreen change detection to automatically trigger ExitPresentMode
1687
     * when user exits fullscreen mode
1688
     */
1689
    private setupFullscreenChangeHandler() {
1690
        const embedConfig = getEmbedConfig();
1691
        const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
25✔
1692

25!
1693
        if (disableFullscreenPresentation) {
1694
            return;
25✔
1695
        }
24✔
1696

1697
        if (this.fullscreenChangeHandler) {
1698
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
1!
UNCOV
1699
        }
×
1700

1701
        this.fullscreenChangeHandler = () => {
1702
            const isFullscreen = !!document.fullscreenElement;
1✔
1703
            if (!isFullscreen) {
×
1704
                logger.info('Exited fullscreen mode - triggering ExitPresentMode');
×
UNCOV
1705
                // Only trigger if iframe is available and contentWindow is
×
1706
                // accessible
1707
                if (this.iFrame && this.iFrame.contentWindow) {
1708
                    this.trigger(HostEvent.ExitPresentMode);
×
UNCOV
1709
                } else {
×
1710
                    logger.debug('Skipping ExitPresentMode - iframe contentWindow not available');
UNCOV
1711
                }
×
1712
            }
1713
        };
1714

1715
        document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
1716
    }
1✔
1717

1718
    /**
1719
     * Remove fullscreen change handler
1720
     */
1721
    private removeFullscreenChangeHandler() {
1722
        if (this.fullscreenChangeHandler) {
1723
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
31!
1724
            this.fullscreenChangeHandler = null;
×
UNCOV
1725
        }
×
1726
    }
1727
}
1728

1729
/**
1730
 * Base class for embedding v1 experience
1731
 * Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
1732
 * which is currently under migration to v2
1733
 * @inheritdoc
1734
 */
1735
export class V1Embed extends TsEmbed {
1736
    protected viewConfig: ViewConfig;
14✔
1737

1738
    constructor(domSelector: DOMSelector, viewConfig: ViewConfig) {
1739
        super(domSelector, viewConfig);
1740
        this.viewConfig = { excludeRuntimeFiltersfromURL: false, ...viewConfig };
290✔
1741
    }
290✔
1742

1743
    /**
1744
     * Render the app in an iframe and set up event handlers
1745
     * @param iframeSrc
1746
     */
1747
    protected renderV1Embed(iframeSrc: string): Promise<any> {
1748
        return this.renderIFrame(iframeSrc);
1749
    }
265✔
1750

1751
    protected getRootIframeSrc(): string {
1752
        const queryParams = this.getEmbedParams();
1753
        let queryString = queryParams;
274✔
1754

274✔
1755
        if (!this.viewConfig.excludeRuntimeParametersfromURL) {
1756
            const runtimeParameters = this.viewConfig.runtimeParameters;
274✔
1757
            const parameterQuery = getRuntimeParameters(runtimeParameters || []);
273✔
1758
            queryString = [parameterQuery, queryParams].filter(Boolean).join('&');
273✔
1759
        }
273✔
1760

1761
        if (!this.viewConfig.excludeRuntimeFiltersfromURL) {
1762
            const runtimeFilters = this.viewConfig.runtimeFilters;
274✔
1763

272✔
1764
            const filterQuery = getFilterQuery(runtimeFilters || []);
1765
            queryString = [filterQuery, queryString].filter(Boolean).join('&');
272✔
1766
        }
272✔
1767
        return this.viewConfig.enableV2Shell_experimental
1768
            ? this.getEmbedBasePath(queryString)
274✔
1769
            : this.getV1EmbedBasePath(queryString);
1770
    }
1771

1772
    /**
1773
     * @inheritdoc
1774
     * @example
1775
     * ```js
1776
     * tsEmbed.on(EmbedEvent.Error, (data) => {
1777
     *   console.error(data);
1778
     * });
1779
     * ```
1780
     * @example
1781
     * ```js
1782
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1783
     *   console.log("Answer save clicked", data);
1784
     * }, {
1785
     *   start: true // This will trigger the callback on start of save
1786
     * });
1787
     * ```
1788
     */
1789
    public on(
1790
        messageType: EmbedEvent,
1791
        callback: MessageCallback,
1792
        options: MessageOptions = { start: false },
1793
    ): typeof TsEmbed.prototype {
126✔
1794
        const eventType = this.getCompatibleEventType(messageType);
1795
        return super.on(eventType, callback, options);
1,541✔
1796
    }
1,541✔
1797

1798
    /**
1799
     * Only for testing purposes.
1800
     * @hidden
1801
     */
1802

1803
    public test__executeCallbacks = this.executeCallbacks;
1804
}
290✔
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