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

thoughtspot / visual-embed-sdk / #2564

07 Oct 2025 10:41AM UTC coverage: 94.075% (-0.07%) from 94.149%
#2564

Pull #325

shivam-kumar-ts
SCAL-236431 corrected related tests
Pull Request #325: SCAL-236431 Add embed initialization state management

1237 of 1397 branches covered (88.55%)

Branch coverage included in aggregate %.

23 of 26 new or added lines in 2 files covered. (88.46%)

43 existing lines in 1 file now uncovered.

3034 of 3143 relevant lines covered (96.53%)

96.06 hits per line

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

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

76
const { version } = pkgInfo;
14✔
77

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

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

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

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

111
    /**
112
     * The key to store the embed instance in the DOM node
113
     */
114
    protected embedNodeKey = '__tsEmbed';
403✔
115

116
    protected isAppInitialized = false;
403✔
117

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

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

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

135
    protected embedConfig: EmbedConfig;
136

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

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

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

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

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

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

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

177
    private defaultHiddenActions = [Action.ReportError];
403✔
178

179
    private resizeObserver: ResizeObserver;
180

181
    protected hostEventClient: HostEventClient;
182

183
    protected isReadyForRenderPromise;
184

185
    /**
186
     * Handler for fullscreen change events
187
     */
188
    private fullscreenChangeHandler: (() => void) | null = null;
403✔
189

190
    /**
191
    * Current initialization state
192
    */
193
    private initState: InitState = InitState.NotStarted;
403✔
194
    
195
    /**
196
    * Promise that resolves when initialization is complete
197
    */
198
    protected initPromise: Promise<void>;
199
    
200
    /**
201
    * Resolve function for the init promise
202
    */
203
    private initPromiseResolve: () => void;
204

205
    /**
206
     * Sets the initialization state and emits events
207
     */
208
    private setInitState(newState: InitState): void {
209
        const previousState = this.initState;
210
        this.initState = newState;
211
        
212
        this.executeCallbacks(EmbedEvent.InitStateChange, {
213
            type: EmbedEvent.InitStateChange,
214
            state: newState,
806✔
215
            previousState,
806✔
216
            timestamp: Date.now(),
217
        });
806✔
218
        
219
        if (newState === InitState.Ready) {
220
            this.initPromiseResolve();
221
        }
222
    }
223

224
    /**
806✔
225
     * Gets the current initialization state
403✔
226
     */
227
    public getInitState(): InitState {
228
        return this.initState;
229
    }
230

231
    /**
232
     * Returns a promise that resolves when initialization is complete
233
     */
3✔
234
    public waitForInit(): Promise<void> {
235
        return this.initPromise;
236
    }
237

238
    /**
239
     * Waits for initialization if needed, otherwise returns immediately
240
     */
1✔
241
    private async ensureInitialized(): Promise<void> {
242
        if (this.initState === InitState.Ready) {
243
            return; 
244
        }
245
        await this.waitForInit();
246
    }
247

357✔
248
    constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
357✔
249
        this.el = getDOMNode(domSelector);
NEW
UNCOV
250
        this.eventHandlerMap = new Map();
×
251
        this.isError = false;
252
        this.viewConfig = {
253
            excludeRuntimeFiltersfromURL: false,
254
            excludeRuntimeParametersfromURL: false,
403✔
255
            ...viewConfig,
403✔
256
        };
403✔
257
        this.registerAppInit();
403✔
258
        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
259
            ...viewConfig,
260
        });
261
        const embedConfig = getEmbedConfig();
262
        this.embedConfig = embedConfig;
403✔
263
    
403✔
264
        this.hostEventClient = new HostEventClient(this.iFrame);
265

266
        this.initPromise = new Promise((resolve) => {
403✔
267
            this.initPromiseResolve = resolve;
403✔
268
        });
269
        this.setInitState(InitState.Initializing);
403✔
270

271
        this.isReadyForRenderPromise = getInitPromise().then(async () => {
403✔
272
            if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
403✔
273
                this.embedConfig.authTriggerContainer = domSelector;
403✔
274
            }
275
            this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
403✔
276
            this.thoughtSpotV2Base = getV2BasePath(embedConfig);
277
            this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
403✔
278
            this.setInitState(InitState.Ready);
403✔
279
        });
74✔
280
    }
281

403✔
282
    /**
403✔
283
     * Handles errors within the SDK
403✔
284
     * @param error The error message or object
403✔
285
     */
NEW
UNCOV
286
    protected handleError(error: string | Record<string, unknown>) {
×
UNCOV
287
        this.isError = true;
×
288
        this.executeCallbacks(EmbedEvent.Error, {
289
            error,
290
        });
291
        // Log error
292
        logger.error(error);
293
    }
294

295
    /**
296
     * Extracts the type field from the event payload
12✔
297
     * @param event The window message event
12✔
298
     */
299
    private getEventType(event: MessageEvent) {
300

301
        return event.data?.type || event.data?.__type;
12✔
302
    }
303

304
    /**
305
     * Extracts the port field from the event payload
306
     * @param event  The window message event
307
     * @returns
308
     */
309
    private getEventPort(event: MessageEvent) {
310
        if (event.ports.length && event.ports[0]) {
1,909!
311
            return event.ports[0];
312
        }
313
        return null;
314
    }
315

316
    /**
317
     * Checks if preauth cache is enabled
318
     * from the view config and embed config
319
     * @returns boolean
1,909✔
320
     */
1,236✔
321
    private isPreAuthCacheEnabled() {
322
        // Disable preauth cache when:
673✔
323
        // 1. overrideOrgId is present since:
324
        //    - cached auth info would be for wrong org
325
        //    - info call response changes for each different overrideOrgId
326
        // 2. disablePreauthCache is explicitly set to true
327
        // 3. FullAppEmbed has primary navbar visible since:
328
        //    - primary navbar requires fresh auth state for navigation
329
        //    - cached auth may not reflect current user permissions
330
        const isDisabled = (
331
            this.viewConfig.overrideOrgId !== undefined
332
            || this.embedConfig.disablePreauthCache === true
333
            || this.isFullAppEmbedWithVisiblePrimaryNavbar()
334
        );
335
        return !isDisabled;
336
    }
337

338
    /**
339
     * Checks if current embed is FullAppEmbed with visible primary navbar
340
     * @returns boolean
377✔
341
     */
342
    private isFullAppEmbedWithVisiblePrimaryNavbar(): boolean {
343
        const appViewConfig = this.viewConfig as any;
344

377✔
345
        // Check if this is a FullAppEmbed (AppEmbed)
346
        // showPrimaryNavbar defaults to true if not explicitly set to false
347
        return (
348
            appViewConfig.embedComponentType === 'AppEmbed'
349
            && appViewConfig.showPrimaryNavbar === true
350
        );
351
    }
352

375✔
353
    /**
354
     * fix for ts7.sep.cl
355
     * will be removed for ts7.oct.cl
356
     * @param event
375✔
357
     * @param eventType
501✔
358
     * @hidden
359
     */
360
    private formatEventData(event: MessageEvent, eventType: string) {
361
        const eventData = {
362
            ...event.data,
363
            type: eventType,
364
        };
365
        if (!eventData.data) {
366
            eventData.data = event.data.payload;
367
        }
368
        return eventData;
369
    }
370

1,909✔
371
    private subscribedListeners: Record<string, any> = {};
372

373
    /**
374
     * Subscribe to network events (online/offline) that should
1,909✔
375
     * work regardless of auth status
450✔
376
     */
377
    private subscribeToNetworkEvents() {
1,909✔
378
        this.unsubscribeToNetworkEvents();
379

380
        const onlineEventListener = (e: Event) => {
403✔
381
            this.trigger(HostEvent.Reload);
382
        };
383
        window.addEventListener('online', onlineEventListener);
384

385
        const offlineEventListener = (e: Event) => {
386
            const offlineWarning = ERROR_MESSAGE.OFFLINE_WARNING;
387
            this.executeCallbacks(EmbedEvent.Error, {
363✔
388
                offlineWarning,
389
            });
363✔
390
            logger.warn(offlineWarning);
265✔
391
        };
392
        window.addEventListener('offline', offlineEventListener);
363✔
393

394
        this.subscribedListeners.online = onlineEventListener;
363✔
UNCOV
395
        this.subscribedListeners.offline = offlineEventListener;
×
UNCOV
396
    }
×
397

398
    /**
UNCOV
399
     * Subscribe to message events that depend on successful iframe setup
×
400
     */
401
    private subscribeToMessageEvents() {
363✔
402
        this.unsubscribeToMessageEvents();
403

363✔
404
        const messageEventListener = (event: MessageEvent<any>) => {
363✔
405
            const eventType = this.getEventType(event);
406
            const eventPort = this.getEventPort(event);
407
            const eventData = this.formatEventData(event, eventType);
408
            if (event.source === this.iFrame.contentWindow) {
409
                this.executeCallbacks(
410
                    eventType,
411
                    processEventData(
356✔
412
                        eventType,
413
                        eventData,
356✔
414
                        this.thoughtSpotHost,
1,909✔
415
                        this.isPreRendered ? this.preRenderWrapper : this.el,
1,909✔
416
                    ),
1,909✔
417
                    eventPort,
1,909✔
418
                );
60✔
419
            }
420
        };
421
        window.addEventListener('message', messageEventListener);
422

423
        this.subscribedListeners.message = messageEventListener;
424
    }
60✔
425
    /**
426
     * Adds event listeners for both network and message events.
427
     * This maintains backward compatibility with the existing method.
428
     * Adds a global event listener to window for "message" events.
429
     * ThoughtSpot detects if a particular event is targeted to this
430
     * embed instance through an identifier contained in the payload,
356✔
431
     * and executes the registered callbacks accordingly.
432
     */
356✔
433
    private subscribeToEvents() {
434
        this.subscribeToNetworkEvents();
435
        this.subscribeToMessageEvents();
436
    }
437

438
    private unsubscribeToNetworkEvents() {
439
        if (this.subscribedListeners.online) {
440
            window.removeEventListener('online', this.subscribedListeners.online);
441
            delete this.subscribedListeners.online;
442
        }
443
        if (this.subscribedListeners.offline) {
6✔
444
            window.removeEventListener('offline', this.subscribedListeners.offline);
6✔
445
            delete this.subscribedListeners.offline;
446
        }
447
    }
448

363✔
449
    private unsubscribeToMessageEvents() {
4✔
450
        if (this.subscribedListeners.message) {
4✔
451
            window.removeEventListener('message', this.subscribedListeners.message);
452
            delete this.subscribedListeners.message;
363✔
453
        }
4✔
454
    }
4✔
455

456
    private unsubscribeToEvents() {
457
        Object.keys(this.subscribedListeners).forEach((key) => {
458
            window.removeEventListener(key, this.subscribedListeners[key]);
459
        });
356✔
460
    }
4✔
461

4✔
462
    protected async getAuthTokenForCookielessInit() {
463
        let authToken = '';
464
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
465

466
        try {
37✔
467
            authToken = await getAuthenticationToken(this.embedConfig);
91✔
468
        } catch (e) {
469
            processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
470
            throw e;
471
        }
472

20✔
473
        return authToken;
20✔
474
    }
475

6✔
476
    protected async getDefaultAppInitData(): Promise<DefaultAppInitData> {
6✔
477
        const authToken = await this.getAuthTokenForCookielessInit();
478
        const customActionsResult = getCustomActions([
2✔
479
            ...(this.viewConfig.customActions || []),
2✔
480
            ...(this.embedConfig.customActions || [])
481
        ]);
482
        if (customActionsResult.errors.length > 0) {
4✔
483
            this.handleError({
484
                type: 'CUSTOM_ACTION_VALIDATION',
485
                message: customActionsResult.errors,
486
            });
20✔
487
        }
18✔
488
        return {
35✔
489
            customisations: getCustomisations(this.embedConfig, this.viewConfig),
36✔
490
            authToken,
491
            runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
18!
UNCOV
492
                ? getRuntimeFilters(this.viewConfig.runtimeFilters)
×
493
                : null,
494
            runtimeParameterParams: this.viewConfig.excludeRuntimeParametersfromURL
495
                ? getRuntimeParameters(this.viewConfig.runtimeParameters || [])
496
                : null,
497
            hiddenHomepageModules: this.viewConfig.hiddenHomepageModules || [],
18✔
498
            reorderedHomepageModules: this.viewConfig.reorderedHomepageModules || [],
499
            hostConfig: this.embedConfig.hostConfig,
500
            hiddenHomeLeftNavItems: this.viewConfig?.hiddenHomeLeftNavItems
18✔
501
                ? this.viewConfig?.hiddenHomeLeftNavItems
502
                : [],
503
            customVariablesForThirdPartyTools:
18✔
504
                this.embedConfig.customVariablesForThirdPartyTools || {},
1!
505
            hiddenListColumns: this.viewConfig.hiddenListColumns || [],
506
            customActions: customActionsResult.actions,
35✔
507
        };
35✔
508
    }
509

72!
510
    protected async getAppInitData() {
3!
511
        return this.getDefaultAppInitData();
512
    }
513

26✔
514
    /**
36✔
515
     * Send Custom style as part of payload of APP_INIT
516
     * @param _
517
     * @param responder
518
     */
519
    private appInitCb = async (_: any, responder: any) => {
520
        try {
20✔
521
            const appInitData = await this.getAppInitData();
522
            this.isAppInitialized = true;
523
            responder({
524
                type: EmbedEvent.APP_INIT,
525
                data: appInitData,
526
            });
527
        } catch (e) {
528
            logger.error(`AppInit failed, Error : ${e?.message}`);
403✔
529
        }
20✔
530
    };
20✔
531

18✔
532
    /**
18✔
533
     * Sends updated auth token to the iFrame to avoid user logout
534
     * @param _
535
     * @param responder
536
     */
537
    private updateAuthToken = async (_: any, responder: any) => {
2!
538
        const { authType } = this.embedConfig;
539
        let { autoLogin } = this.embedConfig;
540
        // Default autoLogin: true for cookieless if undefined/null, otherwise
541
        // false
542
        autoLogin = autoLogin ?? (authType === AuthType.TrustedAuthTokenCookieless);
543
        if (autoLogin && authType === AuthType.TrustedAuthTokenCookieless) {
544
            try {
545
                const authToken = await getAuthenticationToken(this.embedConfig);
546
                responder({
403✔
547
                    type: EmbedEvent.AuthExpire,
10✔
548
                    data: { authToken },
10✔
549
                });
550
            } catch (e) {
551
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
10✔
552
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
10✔
553
            }
5✔
554
        } else if (autoLogin) {
5✔
555
            handleAuth();
3✔
556
        }
557
        notifyAuthFailure(AuthFailureType.EXPIRY);
558
    };
559

560
    /**
2!
561
     * Auto Login and send updated authToken to the iFrame to avoid user session logout
2✔
562
     * @param _
563
     * @param responder
5✔
564
     */
2✔
565
    private idleSessionTimeout = (_: any, responder: any) => {
566
        handleAuth().then(async () => {
10✔
567
            let authToken = '';
568
            try {
569
                authToken = await getAuthenticationToken(this.embedConfig);
570
                responder({
571
                    type: EmbedEvent.IdleSessionTimeout,
572
                    data: { authToken },
573
                });
574
            } catch (e) {
403✔
575
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
3✔
576
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
3✔
577
            }
3✔
578
        }).catch((e) => {
3✔
579
            logger.error(`Auto Login failed, Error : ${e?.message}`);
2✔
580
        });
581
        notifyAuthFailure(AuthFailureType.IDLE_SESSION_TIMEOUT);
582
    };
583

584
    /**
1!
585
     * Register APP_INIT event and sendback init payload
1!
586
     */
587
    private registerAppInit = () => {
UNCOV
588
        this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
×
589
        this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
590
        this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
3✔
591

592
        const embedListenerReadyHandler = this.createEmbedContainerHandler(EmbedEvent.EmbedListenerReady);
593
        this.on(EmbedEvent.EmbedListenerReady, embedListenerReadyHandler, { start: false }, true);
594

595
        const authInitHandler = this.createEmbedContainerHandler(EmbedEvent.AuthInit);
596
        this.on(EmbedEvent.AuthInit, authInitHandler, { start: false }, true);
403✔
597
    };
403✔
598

403✔
599
    /**
403✔
600
     * Constructs the base URL string to load the ThoughtSpot app.
601
     * @param query
403✔
602
     */
403✔
603
    protected getEmbedBasePath(query: string): string {
604
        let queryString = query.startsWith('?') ? query : `?${query}`;
403✔
605
        if (this.shouldEncodeUrlQueryParams) {
403✔
606
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
607
                queryString.substr(1),
608
            )}`;
609
        }
610
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
611
            .filter((x) => x.length > 0)
612
            .join('/');
613

114✔
614
        return `${basePath}#`;
114✔
615
    }
1✔
616

617
    protected getUpdateEmbedParamsObject() {
618
        let queryParams = this.getEmbedParamsObject();
619
        queryParams = { ...this.viewConfig, ...queryParams };
114✔
620
        return queryParams;
342✔
621
    }
622

623
    /**
114✔
624
     * Common query params set for all the embed modes.
625
     * @param queryParams
626
     * @returns queryParams
627
     */
2✔
628
    protected getBaseQueryParams(queryParams: Record<any, any> = {}) {
2✔
629
        let hostAppUrl = window?.location?.host || '';
2✔
630

631
        // The below check is needed because TS Cloud firewall, blocks
632
        // localhost/127.0.0.1 in any url param.
633
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
634
            hostAppUrl = 'local-host';
635
        }
636
        const blockNonEmbedFullAppAccess = this.embedConfig.blockNonEmbedFullAppAccess ?? true;
637
        queryParams[Param.EmbedApp] = true;
129✔
638
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
368!
639
        queryParams[Param.ViewPortHeight] = window.innerHeight;
640
        queryParams[Param.ViewPortWidth] = window.innerWidth;
641
        queryParams[Param.Version] = version;
642
        queryParams[Param.AuthType] = this.embedConfig.authType;
368!
643
        queryParams[Param.blockNonEmbedFullAppAccess] = blockNonEmbedFullAppAccess;
368✔
644
        queryParams[Param.AutoLogin] = this.embedConfig.autoLogin;
645
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
368✔
646
            queryParams[Param.DisableLoginRedirect] = true;
368✔
647
        }
368✔
648
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
368✔
649
            queryParams[Param.ForceSAMLAutoRedirect] = true;
368✔
650
        }
368✔
651
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
368✔
652
            queryParams[Param.cookieless] = true;
368✔
653
        }
368✔
654
        if (this.embedConfig.pendoTrackingKey) {
368✔
655
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
8✔
656
        }
657
        if (this.embedConfig.numberFormatLocale) {
368!
UNCOV
658
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
×
659
        }
660
        if (this.embedConfig.dateFormatLocale) {
368✔
661
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
16✔
662
        }
663
        if (this.embedConfig.currencyFormat) {
368✔
664
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
1✔
665
        }
666

368✔
667
        const {
13✔
668
            disabledActions,
669
            disabledActionReason,
368✔
670
            hiddenActions,
13✔
671
            visibleActions,
672
            hiddenTabs,
368✔
673
            visibleTabs,
13✔
674
            showAlerts,
675
            additionalFlags: additionalFlagsFromView,
676
            locale,
677
            customizations,
678
            contextMenuTrigger,
679
            linkOverride,
680
            insertInToSlide,
681
            disableRedirectionLinksInNewTab,
682
            overrideOrgId,
683
            exposeTranslationIDs,
684
            primaryAction,
685
        } = this.viewConfig;
686

687
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
688

689
        const additionalFlags = {
690
            ...additionalFlagsFromInit,
691
            ...additionalFlagsFromView,
692
        };
693

694
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
368✔
695
            this.handleError('You cannot have both hidden actions and visible actions');
696
            return queryParams;
368✔
697
        }
698

368✔
699
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
700
            this.handleError('You cannot have both hidden Tabs and visible Tabs');
701
            return queryParams;
702
        }
703
        if (primaryAction) {
368✔
704
            queryParams[Param.PrimaryAction] = primaryAction;
4✔
705
        }
4✔
706

707
        if (disabledActions?.length) {
708
            queryParams[Param.DisableActions] = disabledActions;
364✔
709
        }
4✔
710
        if (disabledActionReason) {
4✔
711
            queryParams[Param.DisableActionReason] = disabledActionReason;
712
        }
360!
UNCOV
713
        if (exposeTranslationIDs) {
×
714
            queryParams[Param.ExposeTranslationIDs] = exposeTranslationIDs;
715
        }
716
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
360✔
717
        if (Array.isArray(visibleActions)) {
6✔
718
            queryParams[Param.VisibleActions] = visibleActions;
719
        }
360✔
720
        if (Array.isArray(hiddenTabs)) {
6✔
721
            queryParams[Param.HiddenTabs] = hiddenTabs;
722
        }
360✔
723
        if (Array.isArray(visibleTabs)) {
1✔
724
            queryParams[Param.VisibleTabs] = visibleTabs;
725
        }
360✔
726
        /**
360✔
727
         * Default behavior for context menu will be left-click
5✔
728
         *  from version 9.2.0.cl the user have an option to override context
729
         *  menu click
360✔
730
         */
2✔
731
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
732
            queryParams[Param.ContextMenuTrigger] = 'left';
360✔
733
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
1✔
734
            queryParams[Param.ContextMenuTrigger] = 'right';
735
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.BOTH_CLICKS) {
736
            queryParams[Param.ContextMenuTrigger] = 'both';
737
        }
738

739
        const embedCustomizations = this.embedConfig.customizations;
740
        const spriteUrl = customizations?.iconSpriteUrl || embedCustomizations?.iconSpriteUrl;
360✔
741
        if (spriteUrl) {
3✔
742
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
357✔
743
        }
2✔
744

355✔
745
        const stringIDsUrl = customizations?.content?.stringIDsUrl
2✔
746
            || embedCustomizations?.content?.stringIDsUrl;
747
        if (stringIDsUrl) {
748
            queryParams[Param.StringIDsUrl] = stringIDsUrl;
360✔
749
        }
360✔
750

360✔
751
        if (showAlerts !== undefined) {
1✔
752
            queryParams[Param.ShowAlerts] = showAlerts;
753
        }
754
        if (locale !== undefined) {
360✔
755
            queryParams[Param.Locale] = locale;
2,160✔
756
        }
360✔
757

2✔
758
        if (linkOverride) {
759
            queryParams[Param.LinkOverride] = linkOverride;
760
        }
360✔
761
        if (insertInToSlide) {
1✔
762
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
763
        }
360✔
764
        if (disableRedirectionLinksInNewTab) {
1✔
765
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
766
        }
767
        if (overrideOrgId !== undefined) {
360!
UNCOV
768
            queryParams[Param.OverrideOrgId] = overrideOrgId;
×
769
        }
770

360!
UNCOV
771
        if (this.isPreAuthCacheEnabled()) {
×
772
            queryParams[Param.preAuthCache] = true;
773
        }
360!
UNCOV
774

×
775
        queryParams[Param.OverrideNativeConsole] = true;
776
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
360✔
777

3✔
778
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
779
            Object.assign(queryParams, additionalFlags);
780
        }
360✔
781

351✔
782
        // Do not add any flags below this, as we want additional flags to
783
        // override other flags
784

360✔
785
        return queryParams;
360✔
786
    }
787

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

804
    protected getEmbedParams() {
805
        const queryParams = this.getEmbedParamsObject();
252✔
806
        return getQueryParamString(queryParams);
807
    }
808

252✔
809
    protected getEmbedParamsObject() {
252✔
810
        const params = this.getBaseQueryParams();
252✔
811
        return params;
812
    }
813

UNCOV
814
    protected getRootIframeSrc() {
×
UNCOV
815
        const query = this.getEmbedParams();
×
816
        return this.getEmbedBasePath(query);
817
    }
818

UNCOV
819
    protected createIframeEl(frameSrc: string): HTMLIFrameElement {
×
UNCOV
820
        const iFrame = document.createElement('iframe');
×
821

822
        iFrame.src = frameSrc;
823
        iFrame.id = TS_EMBED_ID;
824
        iFrame.setAttribute('data-ts-iframe', 'true');
91✔
825

91✔
826
        // according to screenfull.js documentation
827
        // allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be
828
        // true
829
        iFrame.allowFullscreen = true;
342✔
830
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
831
        // @ts-ignore
342✔
832
        iFrame.webkitallowfullscreen = true;
342✔
833
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
342✔
834
        // @ts-ignore
835
        iFrame.mozallowfullscreen = true;
836
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
837
        // @ts-ignore
838
        iFrame.allow = 'clipboard-read; clipboard-write; fullscreen;';
342✔
839

840
        const frameParams = this.viewConfig.frameParams;
841
        const { height: frameHeight, width: frameWidth, ...restParams } = frameParams || {};
342✔
842
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
843
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
844
        setAttributes(iFrame, restParams);
342✔
845

846
        iFrame.style.width = `${width}`;
847
        iFrame.style.height = `${height}`;
342✔
848
        // Set minimum height to the frame so that,
849
        // scaling down on the fullheight doesn't make it too small.
342✔
850
        iFrame.style.minHeight = `${height}`;
342✔
851
        iFrame.style.border = '0';
342✔
852
        iFrame.name = 'ThoughtSpot Embedded Analytics';
342✔
853
        return iFrame;
342✔
854
    }
855

342✔
856
    protected handleInsertionIntoDOM(child: string | Node): void {
342✔
857
        if (this.isPreRendered) {
858
            this.insertIntoDOMForPreRender(child);
859
        } else {
342✔
860
            this.insertIntoDOM(child);
342✔
861
        }
342✔
862
        if (this.insertedDomEl instanceof Node) {
342✔
863
            (this.insertedDomEl as any)[this.embedNodeKey] = this;
864
        }
865
    }
866

357✔
867
    /**
12✔
868
     * Renders the embedded ThoughtSpot app in an iframe and sets up
869
     * event listeners.
345✔
870
     * @param url - The URL of the embedded ThoughtSpot app.
871
     */
357✔
872
    protected async renderIFrame(url: string): Promise<any> {
352✔
873
        if (this.isError) {
874
            return null;
875
        }
876
        // Wait for initialization instead of throwing error
877
        try {
878
            await this.ensureInitialized();
879
        } catch (error) {
880
            this.handleError('Cannot render: initialization failed');
881
            return null;
882
        }
365✔
883
        if (url.length > URL_MAX_LENGTH) {
8✔
884
            // warn: The URL is too long
885
        }
886

357✔
887
        return renderInQueue((nextInQueue) => {
357✔
888
            const initTimestamp = Date.now();
NEW
UNCOV
889

×
UNCOV
890
            this.executeCallbacks(EmbedEvent.Init, {
×
891
                data: {
892
                    timestamp: initTimestamp,
357!
893
                },
894
                type: EmbedEvent.Init,
895
            });
896

357✔
897
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
357✔
898

899
            // Always subscribe to network events, regardless of auth status
357✔
900
            this.subscribeToNetworkEvents();
901

902
            return getAuthPromise()
903
                ?.then((isLoggedIn: boolean) => {
904
                    if (!isLoggedIn) {
905
                        this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
906
                        return;
357✔
907
                    }
908

909
                    this.setIframeElement(this.iFrame || this.createIframeEl(url));
357✔
910
                    this.iFrame.addEventListener('load', () => {
911
                        nextInQueue();
357!
912
                        const loadTimestamp = Date.now();
913
                        this.executeCallbacks(EmbedEvent.Load, {
353✔
914
                            data: {
3✔
915
                                timestamp: loadTimestamp,
3✔
916
                            },
917
                            type: EmbedEvent.Load,
918
                        });
350✔
919
                        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE, {
350✔
920
                            elWidth: this.iFrame.clientWidth,
17✔
921
                            elHeight: this.iFrame.clientHeight,
17✔
922
                            timeTookToLoad: loadTimestamp - initTimestamp,
17✔
923
                        });
924
                        // Send info event  if preauth cache is enabled
925
                        if (this.isPreAuthCacheEnabled()) {
926
                            getPreauthInfo().then((data) => {
927
                                if (data?.info) {
928
                                    this.trigger(HostEvent.InfoSuccess, data);
17✔
929
                                }
930
                            });
931
                        }
932

933
                        // Setup fullscreen change handler after iframe is
934
                        // loaded and ready
17✔
935
                        this.setupFullscreenChangeHandler();
13✔
936
                    });
13✔
937
                    this.iFrame.addEventListener('error', () => {
5✔
938
                        nextInQueue();
939
                    });
940
                    this.handleInsertionIntoDOM(this.iFrame);
941
                    const prefetchIframe = document.querySelectorAll('.prefetchIframe');
942
                    if (prefetchIframe.length) {
943
                        prefetchIframe.forEach((el) => {
944
                            el.remove();
17✔
945
                        });
946
                    }
350✔
947
                    // Subscribe to message events only after successful
1✔
948
                    // auth and iframe setup
949
                    this.subscribeToMessageEvents();
350✔
950
                })
350✔
951
                .catch((error) => {
350!
UNCOV
952
                    nextInQueue();
×
UNCOV
953
                    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_FAILED, {
×
954
                        error: JSON.stringify(error),
955
                    });
956
                    this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
957
                    this.handleError(error);
958
                });
350✔
959
        });
960
    }
961

4✔
962
    protected createPreRenderWrapper(): HTMLDivElement {
4✔
963
        const preRenderIds = this.getPreRenderIds();
964

965
        document.getElementById(preRenderIds.wrapper)?.remove();
4✔
966

4✔
967
        const preRenderWrapper = document.createElement('div');
968
        preRenderWrapper.id = preRenderIds.wrapper;
969
        const initialPreRenderWrapperStyle = {
970
            position: 'absolute',
971
            width: '100vw',
972
            height: '100vh',
12✔
973
        };
974
        setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
12✔
975

976
        return preRenderWrapper;
12✔
977
    }
12✔
978

12✔
979
    protected preRenderWrapper: HTMLElement;
980

981
    protected preRenderChild: HTMLElement;
982

983
    protected connectPreRendered(): boolean {
12✔
984
        const preRenderIds = this.getPreRenderIds();
985
        const preRenderWrapperElement = document.getElementById(preRenderIds.wrapper);
12✔
986
        this.preRenderWrapper = this.preRenderWrapper || preRenderWrapperElement;
987

988
        this.preRenderChild = this.preRenderChild || document.getElementById(preRenderIds.child);
989

990
        if (this.preRenderWrapper && this.preRenderChild) {
991
            this.isPreRendered = true;
992
            if (this.preRenderChild instanceof HTMLIFrameElement) {
993
                this.setIframeElement(this.preRenderChild);
17✔
994
            }
17✔
995
            this.insertedDomEl = this.preRenderWrapper;
17✔
996
            this.isRendered = true;
997
        }
17✔
998

999
        return this.isPreRenderAvailable();
17✔
1000
    }
4✔
1001

4✔
1002
    protected isPreRenderAvailable(): boolean {
4✔
1003
        return (
1004
            this.isRendered
4✔
1005
            && this.isPreRendered
4✔
1006
            && Boolean(this.preRenderWrapper && this.preRenderChild)
1007
        );
1008
    }
17✔
1009

1010
    protected createPreRenderChild(child: string | Node): HTMLElement {
1011
        const preRenderIds = this.getPreRenderIds();
1012

44✔
1013
        document.getElementById(preRenderIds.child)?.remove();
92✔
1014

1015
        if (child instanceof HTMLElement) {
48✔
1016
            child.id = preRenderIds.child;
1017
            return child;
1018
        }
1019

1020
        const divChildNode = document.createElement('div');
12✔
1021
        setStyleProperties(divChildNode, { width: '100%', height: '100%' });
1022
        divChildNode.id = preRenderIds.child;
12✔
1023

1024
        if (typeof child === 'string') {
12✔
1025
            divChildNode.innerHTML = child;
11✔
1026
        } else {
11✔
1027
            divChildNode.appendChild(child);
1028
        }
1029

1✔
1030
        return divChildNode;
1✔
1031
    }
1✔
1032

1033
    protected insertIntoDOMForPreRender(child: string | Node): void {
1!
1034
        const preRenderChild = this.createPreRenderChild(child);
1✔
1035
        const preRenderWrapper = this.createPreRenderWrapper();
UNCOV
1036
        preRenderWrapper.appendChild(preRenderChild);
×
1037

1038
        this.preRenderChild = preRenderChild;
1039
        this.preRenderWrapper = preRenderWrapper;
1✔
1040

1041
        if (preRenderChild instanceof HTMLIFrameElement) {
1042
            this.setIframeElement(preRenderChild);
1043
        }
12✔
1044
        this.insertedDomEl = preRenderWrapper;
12✔
1045

12✔
1046
        if (this.showPreRenderByDefault) {
1047
            this.showPreRender();
12✔
1048
        } else {
12✔
1049
            this.hidePreRender();
1050
        }
12✔
1051

11✔
1052
        document.body.appendChild(preRenderWrapper);
1053
    }
12✔
1054

1055
    private showPreRenderByDefault = false;
12✔
1056

2✔
1057
    protected insertIntoDOM(child: string | Node): void {
1058
        if (this.viewConfig.insertAsSibling) {
10✔
1059
            if (typeof child === 'string') {
1060
                const div = document.createElement('div');
1061
                div.innerHTML = child;
12✔
1062
                div.id = TS_EMBED_ID;
1063

1064
                child = div;
403✔
1065
            }
1066
            if (this.el.nextElementSibling?.id === TS_EMBED_ID) {
1067
                this.el.nextElementSibling.remove();
345✔
1068
            }
12✔
1069
            this.el.parentElement.insertBefore(child, this.el.nextSibling);
1✔
1070
            this.insertedDomEl = child;
1✔
1071
        } else if (typeof child === 'string') {
1✔
1072
            this.el.innerHTML = child;
1073
            this.insertedDomEl = this.el.children[0];
1✔
1074
        } else {
1075
            this.el.innerHTML = '';
12✔
1076
            this.el.appendChild(child);
1✔
1077
            this.insertedDomEl = child;
1078
        }
12✔
1079
    }
12✔
1080

333✔
1081
    /**
5✔
1082
     * Sets the height of the iframe
5✔
1083
     * @param height The height in pixels
1084
     */
328✔
1085
    protected setIFrameHeight(height: number | string): void {
328✔
1086
        this.iFrame.style.height = getCssDimension(height);
328✔
1087
    }
1088

1089
    /**
1090
     * Executes all registered event handlers for a particular event type
1091
     * @param eventType The event type
1092
     * @param data The payload invoked with the event handler
1093
     * @param eventPort The event Port for a specific MessageChannel
1094
     */
1095
    protected executeCallbacks(
2✔
1096
        eventType: EmbedEvent,
1097
        data: any,
1098
        eventPort?: MessagePort | void,
1099
    ): void {
1100
        const eventHandlers = this.eventHandlerMap.get(eventType) || [];
1101
        const allHandlers = this.eventHandlerMap.get(EmbedEvent.ALL) || [];
1102
        const callbacks = [...eventHandlers, ...allHandlers];
1103
        const dataStatus = data?.status || embedEventStatus.END;
1104
        callbacks.forEach((callbackObj) => {
1105
            if (
1106
                // When start status is true it trigger only start releated
1107
                // payload
1108
                (callbackObj.options.start && dataStatus === embedEventStatus.START)
1109
                // When start status is false it trigger only end releated
1,253✔
1110
                // payload
1,253✔
1111
                || (!callbackObj.options.start && dataStatus === embedEventStatus.END)
1,253✔
1112
            ) {
1,253!
1113
                callbackObj.callback(data, (payload) => {
1,253✔
1114
                    this.triggerEventOnPort(eventPort, payload);
62✔
1115
                });
1116
            }
1117
        });
185✔
1118
    }
1119

1120
    /**
1121
     * Returns the ThoughtSpot hostname or IP address.
1122
     */
61✔
1123
    protected getThoughtSpotHost(): string {
27✔
1124
        return this.thoughtSpotHost;
1125
    }
1126

1127
    /**
1128
     * Gets the v1 event type (if applicable) for the EmbedEvent type
1129
     * @param eventType The v2 event type
1130
     * @returns The corresponding v1 event type if one exists
1131
     * or else the v2 event type itself
1132
     */
UNCOV
1133
    protected getCompatibleEventType(eventType: EmbedEvent): EmbedEvent {
×
1134
        return V1EventMap[eventType] || eventType;
1135
    }
1136

1137
    /**
1138
     * Calculates the iframe center for the current visible viewPort
1139
     * of iframe using Scroll position of Host App, offsetTop for iframe
1140
     * in Host app. ViewPort height of the tab.
1141
     * @returns iframe Center in visible viewport,
1142
     *  Iframe height,
1143
     *  View port height.
1,436✔
1144
     */
1145
    protected getIframeCenter() {
1146
        const offsetTopClient = getOffsetTop(this.iFrame);
1147
        const scrollTopClient = window.scrollY;
1148
        const viewPortHeight = window.innerHeight;
1149
        const iframeHeight = this.iFrame.offsetHeight;
1150
        const iframeScrolled = scrollTopClient - offsetTopClient;
1151
        let iframeVisibleViewPort;
1152
        let iframeOffset;
1153

1154
        if (iframeScrolled < 0) {
1155
            iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
4✔
1156
            iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
4✔
1157
            iframeOffset = 0;
4✔
1158
        } else {
4✔
1159
            iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
4✔
1160
            iframeOffset = iframeScrolled;
1161
        }
1162
        const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
1163
        return {
4!
UNCOV
1164
            iframeCenter,
×
UNCOV
1165
            iframeScrolled,
×
UNCOV
1166
            iframeHeight,
×
1167
            viewPortHeight,
1168
            iframeVisibleViewPort,
4✔
1169
        };
4✔
1170
    }
1171

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

1213
    /**
2,123✔
1214
     * Removes an event listener for a particular event type.
2✔
1215
     * @param messageType The message type
1216
     * @param callback The callback to remove
2,123✔
1217
     * @example
2,123✔
1218
     * ```js
2,123✔
1219
     * const errorHandler = (data) => { console.error(data); };
2,123✔
1220
     * tsEmbed.on(EmbedEvent.Error, errorHandler);
1221
     * tsEmbed.off(EmbedEvent.Error, errorHandler);
1222
     * ```
1223
     */
1224
    public off(messageType: EmbedEvent, callback: MessageCallback): typeof TsEmbed.prototype {
1225
        const callbacks = this.eventHandlerMap.get(messageType) || [];
1226
        const index = callbacks.findIndex((cb) => cb.callback === callback);
1227
        if (index > -1) {
1228
            callbacks.splice(index, 1);
1229
        }
1230
        return this;
1231
    }
1232

1233
    /**
1234
     * Triggers an event on specific Port registered against
1!
1235
     * for the EmbedEvent
1✔
1236
     * @param eventType The message type
1✔
1237
     * @param data The payload to send
1✔
1238
     * @param eventPort
1239
     * @param payload
1✔
1240
     */
1241
    private triggerEventOnPort(eventPort: MessagePort | void, payload: any) {
1242
        if (eventPort) {
1243
            try {
1244
                eventPort.postMessage({
1245
                    type: payload.type,
1246
                    data: payload.data,
1247
                });
1248
            } catch (e) {
1249
                eventPort.postMessage({ error: e });
1250
                logger.log(e);
1251
            }
27✔
1252
        } else {
23✔
1253
            logger.log('Event Port is not defined');
23✔
1254
        }
1255
    }
1256

1257
     /**
UNCOV
1258
     * @hidden
×
UNCOV
1259
     * Internal state to track if the embed container is loaded.
×
1260
     * This is used to trigger events after the embed container is loaded.
1261
     */
1262
     public isEmbedContainerLoaded = false;
4✔
1263

1264
    /**
1265
     * @hidden
1266
     * Internal state to track the callbacks to be executed after the embed container 
1267
     * is loaded.
1268
     * This is used to trigger events after the embed container is loaded.
1269
     */
1270
    private embedContainerReadyCallbacks: Array<() => void> = [];
1271

403✔
1272
    protected getPreRenderObj<T extends TsEmbed>(): T {
1273
        const embedObj = (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
1274
        if (embedObj === (this as any)) {
1275
            logger.info('embedObj is same as this');
1276
        }
1277
        return embedObj;
1278
    }
1279

403✔
1280
    private checkEmbedContainerLoaded() {
1281
        if (this.isEmbedContainerLoaded) return true;
1282

30✔
1283
        const preRenderObj = this.getPreRenderObj<TsEmbed>();
30✔
1284
        if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
5✔
1285
            this.isEmbedContainerLoaded = true;
1286
        }
30✔
1287

1288
        return this.isEmbedContainerLoaded;
1289
    }
1290
    private executeEmbedContainerReadyCallbacks() {
28✔
1291
        logger.debug('executePendingEvents', this.embedContainerReadyCallbacks);
1292
        this.embedContainerReadyCallbacks.forEach((callback) => {
22✔
1293
            callback?.();
22✔
1294
        });
2✔
1295
        this.embedContainerReadyCallbacks = [];
1296
    }
1297

22✔
1298
    /**
1299
     * Executes a callback after the embed container is loaded.
1300
     * @param callback The callback to execute
14✔
1301
     */
14✔
1302
    protected executeAfterEmbedContainerLoaded(callback: () => void) {
12!
1303
        if (this.checkEmbedContainerLoaded()) {
1304
            callback?.();
14✔
1305
        } else {
1306
            logger.debug('pushing callback to embedContainerReadyCallbacks', callback);
1307
            this.embedContainerReadyCallbacks.push(callback);
1308
        }
1309
    }
1310

1311
    protected createEmbedContainerHandler = (source: EmbedEvent.AuthInit | EmbedEvent.EmbedListenerReady) => () => {
1312
        const processEmbedContainerReady = () => {
27✔
1313
            logger.debug('processEmbedContainerReady');
7!
1314
            this.isEmbedContainerLoaded = true;
1315
            this.executeEmbedContainerReadyCallbacks();
20✔
1316
        }
20✔
1317
        if (source === EmbedEvent.AuthInit) {
1318
            const AUTH_INIT_FALLBACK_DELAY = 1000;
1319
            // Wait for 1 second to ensure the embed container is loaded
1320
            // This is a workaround to ensure the embed container is loaded
808✔
1321
            // this is needed until all clusters have EmbedListenerReady event
10✔
1322
            setTimeout(processEmbedContainerReady, AUTH_INIT_FALLBACK_DELAY);
9✔
1323
        } else if (source === EmbedEvent.EmbedListenerReady) {
9✔
1324
            processEmbedContainerReady();
9✔
1325
        }
1326
    }
10✔
1327

7✔
1328
    /**
1329
     * Triggers an event to the embedded app
1330
     * @param {HostEvent} messageType The event type
1331
     * @param {any} data The payload to send with the message
7✔
1332
     * @returns A promise that resolves with the response from the embedded app
3✔
1333
     */
3✔
1334
    public async trigger<HostEventT extends HostEvent, PayloadT>(
1335
        messageType: HostEventT,
1336
        data: TriggerPayload<PayloadT, HostEventT> = {} as any,
1337
    ): Promise<TriggerResponse<PayloadT, HostEventT>> {
1338
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
1339

1340
        if (!this.isRendered) {
1341
            this.handleError('Please call render before triggering events');
1342
            return null;
1343
        }
1344

1345
        if (!messageType) {
204✔
1346
            this.handleError('Host event type is undefined');
1347
            return null;
289✔
1348
        }
1349

289!
UNCOV
1350
        // Check if iframe exists before triggering - 
×
UNCOV
1351
        // this prevents the error when auth fails
×
1352
        if (!this.iFrame) {
1353
            logger.debug(
1354
                `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
289!
UNCOV
1355
            );
×
UNCOV
1356
            return null;
×
1357
        }
1358

1359
        // send an empty object, this is needed for liveboard default handlers
1360
        return this.hostEventClient.triggerHostEvent(messageType, data);
1361
    }
289✔
1362

20✔
1363
    /**
1364
     * Triggers an event to the embedded app, skipping the UI flow.
1365
     * @param {UIPassthroughEvent} apiName - The name of the API to be triggered.
20✔
1366
     * @param {UIPassthroughRequest} parameters - The parameters to be passed to the API.
1367
     * @returns {Promise<UIPassthroughRequest>} - A promise that resolves with the response
1368
     * from the embedded app.
1369
     */
269✔
1370
    public async triggerUIPassThrough<UIPassthroughEventT extends UIPassthroughEvent>(
1371
        apiName: UIPassthroughEventT,
1372
        parameters: UIPassthroughRequest<UIPassthroughEventT>,
1373
    ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> {
1374
        const response = this.hostEventClient.triggerUIPassthroughApi(apiName, parameters);
1375
        return response;
1376
    }
1377

1378
    /**
1379
     * Marks the ThoughtSpot object to have been rendered
1380
     * Needs to be overridden by subclasses to do the actual
1381
     * rendering of the iframe.
1382
     * @param args
1383
     */
1✔
1384
    public async render(): Promise<TsEmbed> {
1✔
1385
        if (!getIsInitCalled()) {
1386
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1387
        }
1388
        await this.isReadyForRenderPromise;
1389
        this.isRendered = true;
1390

1391
        return this;
1392
    }
1393

1394
    public getIframeSrc(): string {
359✔
1395
        return '';
2✔
1396
    }
1397

359✔
1398
    protected handleRenderForPrerender() {
359✔
1399
        return this.render();
1400
    }
359✔
1401

1402
    /**
1403
     * Creates the preRender shell
UNCOV
1404
     * @param showPreRenderByDefault - Show the preRender after render, hidden by default
×
1405
     */
1406

1407
    public async preRender(showPreRenderByDefault = false, replaceExistingPreRender = false): Promise<TsEmbed> {
1408
        if (!this.viewConfig.preRenderId) {
7✔
1409
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1410
            return this;
1411
        }
1412
        this.isPreRendered = true;
1413
        this.showPreRenderByDefault = showPreRenderByDefault;
1414
        
1415
        const isAlreadyRendered = this.connectPreRendered();
1416
        if (isAlreadyRendered && !replaceExistingPreRender) {
22✔
1417
            return this;
13✔
1418
        }
1✔
1419

1✔
1420
        return this.handleRenderForPrerender();
1421
    }
12✔
1422

12✔
1423
    /**
1424
     * Get the Post Url Params for THOUGHTSPOT from the current
12✔
1425
     * host app URL.
12!
UNCOV
1426
     * THOUGHTSPOT URL params starts with a prefix "ts-"
×
1427
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
1428
     */
1429
    public getThoughtSpotPostUrlParams(
12✔
1430
        additionalParams: { [key: string]: string | number } = {},
1431
    ): string {
1432
        const urlHash = window.location.hash;
1433
        const queryParams = window.location.search;
1434
        const postHashParams = urlHash.split('?');
1435
        const postURLParams = postHashParams[postHashParams.length - 1];
1436
        const queryParamsObj = new URLSearchParams(queryParams);
1437
        const postURLParamsObj = new URLSearchParams(postURLParams);
1438
        const params = new URLSearchParams();
1439

345✔
1440
        const addKeyValuePairCb = (value: string, key: string): void => {
1441
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
364✔
1442
                params.append(key, value);
364✔
1443
            }
364✔
1444
        };
364✔
1445
        queryParamsObj.forEach(addKeyValuePairCb);
364✔
1446
        postURLParamsObj.forEach(addKeyValuePairCb);
364✔
1447
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
364✔
1448

1449
        let tsParams = params.toString();
364✔
1450
        tsParams = tsParams ? `?${tsParams}` : '';
8✔
1451

5✔
1452
        return tsParams;
1453
    }
1454

364✔
1455
    /**
364✔
1456
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
364✔
1457
     * @version SDK: 1.19.1 | ThoughtSpot: *
1458
     */
364✔
1459
    public destroy(): void {
364✔
1460
        try {
1461
            this.removeFullscreenChangeHandler();
364✔
1462
            this.insertedDomEl?.parentNode.removeChild(this.insertedDomEl);
1463
            this.unsubscribeToEvents();
1464
        } catch (e) {
1465
            logger.log('Error destroying TS Embed', e);
1466
        }
1467
    }
1468

1469
    public getUnderlyingFrameElement(): HTMLIFrameElement {
26✔
1470
        return this.iFrame;
26✔
1471
    }
26✔
1472

26✔
1473
    /**
UNCOV
1474
     * Prerenders a generic instance of the TS component.
×
1475
     * This means without the path but with the flags already applied.
1476
     * This is useful for prerendering the component in the background.
1477
     * @version SDK: 1.22.0
1478
     * @returns
1479
     */
1✔
1480
    public async prerenderGeneric(): Promise<any> {
1481
        if (!getIsInitCalled()) {
1482
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1483
        }
1484
        await this.isReadyForRenderPromise;
1485

1486
        const prerenderFrameSrc = this.getRootIframeSrc();
1487
        this.isRendered = true;
1488
        return this.renderIFrame(prerenderFrameSrc);
1489
    }
1490

8✔
1491
    protected beforePrerenderVisible(): void {
1✔
1492
        // Override in subclass
1493
    }
8✔
1494

1495
    private validatePreRenderViewConfig = (viewConfig: ViewConfig) => {
8✔
1496
        const preRenderAllowedKeys = ['preRenderId', 'vizId', 'liveboardId'];
8✔
1497
        const preRenderedObject = (this.insertedDomEl as any)?.[this.embedNodeKey] as TsEmbed;
8✔
1498
        if (!preRenderedObject) return;
1499
        if (viewConfig.preRenderId) {
1500
            const allOtherKeys = Object.keys(viewConfig).filter(
1501
                (key) => !preRenderAllowedKeys.includes(key) && !key.startsWith('on'),
1502
            );
1503

1504
            allOtherKeys.forEach((key: keyof ViewConfig) => {
403✔
1505
                if (
3✔
1506
                    !isUndefined(viewConfig[key])
3!
1507
                    && !isEqual(viewConfig[key], preRenderedObject.viewConfig[key])
3!
1508
                ) {
3✔
1509
                    logger.warn(
3✔
1510
                        `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
18✔
1511
                        + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1512
                        + `but a different value "${JSON.stringify(viewConfig[key])}" `
1513
                        + 'was passed to the Embed component. '
3✔
1514
                        + 'The new value provided is ignored, the value provided during '
10✔
1515
                        + 'preRender is used.',
20✔
1516
                    );
1517
                }
1518
            });
4✔
1519
        }
4!
1520
    };
1521

1522
    /**
1523
     * Displays the PreRender component.
1524
     * If the component is not preRendered, it attempts to create and render it.
1525
     * Also, synchronizes the style of the PreRender component with the embedding
1526
     * element.
1527
     */
1528
    public async showPreRender(): Promise<TsEmbed> {
1529
        if (!this.viewConfig.preRenderId) {
1530
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1531
            return this;
1532
        }
1533
        if (!this.isPreRenderAvailable()) {
1534
            const isAvailable = this.connectPreRendered();
1535

1536
            if (!isAvailable) {
1537
                // if the Embed component is not preRendered , Render it now and
1538
                return this.preRender(true);
9✔
1539
            }
1✔
1540
            this.validatePreRenderViewConfig(this.viewConfig);
1✔
1541
            logger.debug('triggering UpdateEmbedParams', this.viewConfig);
1542
            this.executeAfterEmbedContainerLoaded(() => {
8✔
1543
                this.trigger(HostEvent.UpdateEmbedParams, this.getUpdateEmbedParamsObject());
5✔
1544
            });
1545
        }
5✔
1546

1547
        this.beforePrerenderVisible();
2✔
1548

1549
        if (this.el) {
3✔
1550
            this.syncPreRenderStyle();
3✔
1551
            if (!this.viewConfig.doNotTrackPreRenderSize) {
3✔
1552
                this.resizeObserver = new ResizeObserver((entries) => {
2✔
1553
                    entries.forEach((entry) => {
1554
                        if (entry.contentRect && entry.target === this.el) {
1555
                            setStyleProperties(this.preRenderWrapper, {
1556
                                width: `${entry.contentRect.width}px`,
6✔
1557
                                height: `${entry.contentRect.height}px`,
1558
                            });
6✔
1559
                        }
6✔
1560
                    });
6✔
1561
                });
6✔
1562
                this.resizeObserver.observe(this.el);
1✔
1563
            }
1✔
1564
        }
1✔
1565

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

1568
        this.subscribeToEvents();
1569

1570
        // Setup fullscreen change handler for prerendered components
1571
        if (this.iFrame) {
6✔
1572
            this.setupFullscreenChangeHandler();
1573
        }
1574

1575
        return this;
6✔
1576
    }
1577

6✔
1578
    /**
1579
     * Synchronizes the style properties of the PreRender component with the embedding
1580
     * element. This function adjusts the position, width, and height of the PreRender
6✔
1581
     * component
6✔
1582
     * to match the dimensions and position of the embedding element.
1583
     * @throws {Error} Throws an error if the embedding element (passed as domSelector)
1584
     * is not defined or not found.
6✔
1585
     */
1586
    public syncPreRenderStyle(): void {
1587
        if (!this.isPreRenderAvailable() || !this.el) {
1588
            logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1589
            return;
1590
        }
1591
        const elBoundingClient = this.el.getBoundingClientRect();
1592

1593
        setStyleProperties(this.preRenderWrapper, {
1594
            top: `${elBoundingClient.y + window.scrollY}px`,
1595
            left: `${elBoundingClient.x + window.scrollX}px`,
1596
            width: `${elBoundingClient.width}px`,
7✔
1597
            height: `${elBoundingClient.height}px`,
1✔
1598
        });
1✔
1599
    }
1600

6✔
1601
    /**
1602
     * Hides the PreRender component if it is available.
6✔
1603
     * If the component is not preRendered, it issues a warning.
1604
     */
1605
    public hidePreRender(): void {
1606
        if (!this.isPreRenderAvailable()) {
1607
            // if the embed component is not preRendered , nothing to hide
1608
            logger.warn('PreRender should be called before hiding it using hidePreRender.');
1609
            return;
1610
        }
1611
        const preRenderHideStyles = {
1612
            opacity: '0',
1613
            pointerEvents: 'none',
1614
            zIndex: '-1000',
1615
            position: 'absolute ',
12✔
1616
        };
1617
        setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
1✔
1618

1✔
1619
        if (this.resizeObserver) {
1620
            this.resizeObserver.disconnect();
11✔
1621
        }
1622

1623
        this.unsubscribeToEvents();
1624
    }
1625

1626
    /**
11✔
1627
     * Retrieves unique HTML element IDs for PreRender-related elements.
1628
     * These IDs are constructed based on the provided 'preRenderId' from 'viewConfig'.
11✔
1629
     * @returns {object} An object containing the IDs for the PreRender elements.
1✔
1630
     * @property {string} wrapper - The HTML element ID for the PreRender wrapper.
1631
     * @property {string} child - The HTML element ID for the PreRender child.
1632
     */
11✔
1633
    public getPreRenderIds() {
1634
        return {
1635
            wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
1636
            child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1637
        };
1638
    }
1639

1640
    /**
1641
     * Returns the answerService which can be used to make arbitrary graphql calls on top
1642
     * session.
1643
     * @param vizId [Optional] to get for a specific viz in case of a Liveboard.
51✔
1644
     * @version SDK: 1.25.0 / ThoughtSpot 9.10.0
1645
     */
1646
    public async getAnswerService(vizId?: string): Promise<AnswerService> {
1647
        const { session } = await this.trigger(HostEvent.GetAnswerSession, vizId ? { vizId } : {});
1648
        return new AnswerService(session, null, this.embedConfig.thoughtSpotHost);
1649
    }
1650

1651
    /**
1652
     * Set up fullscreen change detection to automatically trigger ExitPresentMode
1653
     * when user exits fullscreen mode
1654
     */
1655
    private setupFullscreenChangeHandler() {
1656
        const embedConfig = getEmbedConfig();
1!
1657
        const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
1✔
1658

1659
        if (disableFullscreenPresentation) {
1660
            return;
1661
        }
1662

1663
        if (this.fullscreenChangeHandler) {
1664
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
1665
        }
25✔
1666

25!
1667
        this.fullscreenChangeHandler = () => {
1668
            const isFullscreen = !!document.fullscreenElement;
25✔
1669
            if (!isFullscreen) {
24✔
1670
                logger.info('Exited fullscreen mode - triggering ExitPresentMode');
1671
                // Only trigger if iframe is available and contentWindow is
1672
                // accessible
1!
1673
                if (this.iFrame && this.iFrame.contentWindow) {
×
1674
                    this.trigger(HostEvent.ExitPresentMode);
1675
                } else {
1676
                    logger.debug('Skipping ExitPresentMode - iframe contentWindow not available');
1✔
UNCOV
1677
                }
×
UNCOV
1678
            }
×
UNCOV
1679
        };
×
1680

1681
        document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
UNCOV
1682
    }
×
UNCOV
1683

×
1684
    /**
UNCOV
1685
     * Remove fullscreen change handler
×
1686
     */
1687
    private removeFullscreenChangeHandler() {
1688
        if (this.fullscreenChangeHandler) {
1689
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
1690
            this.fullscreenChangeHandler = null;
1✔
1691
        }
1692
    }
1693
}
1694

1695
/**
1696
 * Base class for embedding v1 experience
1697
 * Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
27!
UNCOV
1698
 * which is currently under migration to v2
×
UNCOV
1699
 * @inheritdoc
×
1700
 */
1701
export class V1Embed extends TsEmbed {
1702
    protected viewConfig: ViewConfig;
1703

1704
    constructor(domSelector: DOMSelector, viewConfig: ViewConfig) {
1705
        super(domSelector, viewConfig);
1706
        this.viewConfig = { excludeRuntimeFiltersfromURL: false, ...viewConfig };
1707
    }
1708

1709
    /**
1710
     * Render the app in an iframe and set up event handlers
14✔
1711
     * @param iframeSrc
1712
     */
1713
    protected renderV1Embed(iframeSrc: string): Promise<any> {
1714
        return this.renderIFrame(iframeSrc);
269✔
1715
    }
269✔
1716

1717
    protected getRootIframeSrc(): string {
1718
        const queryParams = this.getEmbedParams();
1719
        let queryString = queryParams;
1720

1721
        if (!this.viewConfig.excludeRuntimeParametersfromURL) {
1722
            const runtimeParameters = this.viewConfig.runtimeParameters;
1723
            const parameterQuery = getRuntimeParameters(runtimeParameters || []);
244✔
1724
            queryString = [parameterQuery, queryParams].filter(Boolean).join('&');
1725
        }
1726

1727
        if (!this.viewConfig.excludeRuntimeFiltersfromURL) {
253✔
1728
            const runtimeFilters = this.viewConfig.runtimeFilters;
253✔
1729

1730
            const filterQuery = getFilterQuery(runtimeFilters || []);
253✔
1731
            queryString = [filterQuery, queryString].filter(Boolean).join('&');
252✔
1732
        }
252✔
1733
        return this.viewConfig.enableV2Shell_experimental
252✔
1734
            ? this.getEmbedBasePath(queryString)
1735
            : this.getV1EmbedBasePath(queryString);
1736
    }
253✔
1737

251✔
1738
    /**
1739
     * @inheritdoc
251✔
1740
     * @example
251✔
1741
     * ```js
1742
     * tsEmbed.on(EmbedEvent.Error, (data) => {
253✔
1743
     *   console.error(data);
1744
     * });
1745
     * ```
1746
     * @example
1747
     * ```js
1748
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1749
     *   console.log("Answer save clicked", data);
1750
     * }, {
1751
     *   start: true // This will trigger the callback on start of save
1752
     * });
1753
     * ```
1754
     */
1755
    public on(
1756
        messageType: EmbedEvent,
1757
        callback: MessageCallback,
1758
        options: MessageOptions = { start: false },
1759
    ): typeof TsEmbed.prototype {
1760
        const eventType = this.getCompatibleEventType(messageType);
1761
        return super.on(eventType, callback, options);
1762
    }
1763

1764
    /**
1765
     * Only for testing purposes.
1766
     * @hidden
1767
     */
126✔
1768

1769
    public test__executeCallbacks = this.executeCallbacks;
1,436✔
1770
}
1,436✔
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