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

thoughtspot / visual-embed-sdk / #2960

10 Dec 2025 05:20AM UTC coverage: 94.184% (-0.2%) from 94.432%
#2960

Pull #375

ruchI9897
added check on margin value
Pull Request #375: added check on margin value

1372 of 1542 branches covered (88.98%)

Branch coverage included in aggregate %.

17 of 22 new or added lines in 3 files covered. (77.27%)

5 existing lines in 1 file now uncovered.

3211 of 3324 relevant lines covered (96.6%)

106.5 hits per line

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

96.94
/src/utils.ts
1
/**
2
 * Copyright (c) 2023
3
 *
4
 * Common utility functions for ThoughtSpot Visual Embed SDK
5
 * @summary Utils
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8

9
import merge from 'ts-deepmerge';
35✔
10
import {
11
    EmbedConfig,
12
    QueryParams,
13
    RuntimeFilter,
14
    CustomisationsInterface,
15
    DOMSelector,
16
    RuntimeParameter,
17
    AllEmbedViewConfig,
18
} from './types';
19
import { logger } from './utils/logger';
35✔
20

21
/**
22
 * Construct a runtime filters query string from the given filters.
23
 * Refer to the following docs for more details on runtime filter syntax:
24
 * https://cloud-docs.thoughtspot.com/admin/ts-cloud/apply-runtime-filter.html
25
 * https://cloud-docs.thoughtspot.com/admin/ts-cloud/runtime-filter-operators.html
26
 * @param runtimeFilters
27
 */
28
export const getFilterQuery = (runtimeFilters: RuntimeFilter[]): string | null => {
35✔
29
    if (runtimeFilters && runtimeFilters.length) {
422✔
30
        const filters = runtimeFilters.map((filter, valueIndex) => {
20✔
31
            const index = valueIndex + 1;
22✔
32
            const filterExpr = [];
22✔
33
            filterExpr.push(`col${index}=${encodeURIComponent(filter.columnName)}`);
22✔
34
            filterExpr.push(`op${index}=${filter.operator}`);
22✔
35
            filterExpr.push(
22✔
36
                filter.values.map((value) => {
37
                    const encodedValue = typeof value === 'bigint' ? value.toString() : value;
23!
38
                    return `val${index}=${encodeURIComponent(String(encodedValue))}`;
23✔
39
                }).join('&'),
40
            );
41

42
            return filterExpr.join('&');
22✔
43
        });
44

45
        return `${filters.join('&')}`;
20✔
46
    }
47

48
    return null;
402✔
49
};
50

51
/**
52
 * Construct a runtime parameter override query string from the given option.
53
 * @param runtimeParameters
54
 */
55
export const getRuntimeParameters = (runtimeParameters: RuntimeParameter[]): string => {
35✔
56
    if (runtimeParameters && runtimeParameters.length) {
421✔
57
        const params = runtimeParameters.map((param, valueIndex) => {
11✔
58
            const index = valueIndex + 1;
14✔
59
            const filterExpr = [];
14✔
60
            filterExpr.push(`param${index}=${encodeURIComponent(param.name)}`);
14✔
61
            filterExpr.push(`paramVal${index}=${encodeURIComponent(param.value)}`);
14✔
62

63
            return filterExpr.join('&');
14✔
64
        });
65

66
        return `${params.join('&')}`;
11✔
67
    }
68

69
    return null;
410✔
70
};
71

72
/**
73
 * Convert a value to a string representation to be sent as a query
74
 * parameter to the ThoughtSpot app.
75
 * @param value Any parameter value
76
 */
77
const serializeParam = (value: any) => {
35✔
78
    // do not serialize primitive types
79
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
15,623✔
80
        return value;
15,044✔
81
    }
82

83
    return JSON.stringify(value);
579✔
84
};
85

86
/**
87
 * Convert a value to a string:
88
 * in case of an array, we convert it to CSV.
89
 * in case of any other type, we directly return the value.
90
 * @param value
91
 */
92
const paramToString = (value: any) => (Array.isArray(value) ? value.join(',') : value);
35!
93

94
/**
95
 * Return a query param string composed from the given params object
96
 * @param queryParams
97
 * @param shouldSerializeParamValues
98
 */
99
export const getQueryParamString = (
35✔
100
    queryParams: QueryParams,
101
    shouldSerializeParamValues = false,
9✔
102
): string => {
103
    const qp: string[] = [];
576✔
104
    const params = Object.keys(queryParams);
576✔
105
    params.forEach((key) => {
576✔
106
        const val = queryParams[key];
16,198✔
107
        if (val !== undefined) {
16,198✔
108
            const serializedValue = shouldSerializeParamValues
15,640✔
109
                ? serializeParam(val)
110
                : paramToString(val);
111
            qp.push(`${key}=${serializedValue}`);
15,640✔
112
        }
113
    });
114

115
    if (qp.length) {
576✔
116
        return qp.join('&');
575✔
117
    }
118

119
    return null;
1✔
120
};
121

122
/**
123
 * Get a string representation of a dimension value in CSS
124
 * If numeric, it is considered in pixels.
125
 * @param value
126
 */
127
export const getCssDimension = (value: number | string): string => {
35✔
128
    if (typeof value === 'number') {
809✔
129
        return `${value}px`;
574✔
130
    }
131

132
    return value;
235✔
133
};
134

135
/**
136
 * Validates if a string is a valid CSS margin value.
137
 * @param value - The string to validate
138
 * @returns true if the value is a valid CSS margin value, false otherwise
139
 */
140
export const isValidCssMargin = (value: string): boolean => {
35✔
141
    if (typeof value !== 'string' || value.trim() === '') {
2!
NEW
142
        return false;
×
143
    }
144

145
    // Check for CSS length values (e.g., "10px", "1em", "50%")
146
    const cssLengthPattern = /^\d+(\.\d+)?(px|em|rem|%|vh|vw)$/i;
2✔
147
    const parts = value.trim().split(/\s+/);
2✔
148
    
149
    return parts.length <= 4 && parts.every(part => cssLengthPattern.test(part.trim()));
4✔
150
};
151

152
export const getSSOMarker = (markerId: string) => {
35✔
153
    const encStringToAppend = encodeURIComponent(markerId);
18✔
154
    return `tsSSOMarker=${encStringToAppend}`;
18✔
155
};
156

157
/**
158
 * Append a string to a URL's hash fragment
159
 * @param url A URL
160
 * @param stringToAppend The string to append to the URL hash
161
 */
162
export const appendToUrlHash = (url: string, stringToAppend: string) => {
35✔
163
    let outputUrl = url;
12✔
164
    const encStringToAppend = encodeURIComponent(stringToAppend);
12✔
165

166
    const marker = `tsSSOMarker=${encStringToAppend}`;
12✔
167

168
    let splitAdder = '';
12✔
169

170
    if (url.indexOf('#') >= 0) {
12✔
171
        // If second half of hash contains a '?' already add a '&' instead of
172
        // '?' which appends to query params.
173
        splitAdder = url.split('#')[1].indexOf('?') >= 0 ? '&' : '?';
8✔
174
    } else {
175
        splitAdder = '#?';
4✔
176
    }
177
    outputUrl = `${outputUrl}${splitAdder}${marker}`;
12✔
178

179
    return outputUrl;
12✔
180
};
181

182
/**
183
 *
184
 * @param url
185
 * @param stringToAppend
186
 * @param path
187
 */
188
export function getRedirectUrl(url: string, stringToAppend: string, path = '') {
35✔
189
    const targetUrl = path ? new URL(path, window.location.origin).href : url;
10✔
190
    return appendToUrlHash(targetUrl, stringToAppend);
10✔
191
}
192

193
export const getEncodedQueryParamsString = (queryString: string) => {
35✔
194
    if (!queryString) {
4✔
195
        return queryString;
1✔
196
    }
197
    return btoa(queryString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
3✔
198
};
199

200
export const getOffsetTop = (element: any) => {
35✔
201
    const rect = element.getBoundingClientRect();
5✔
202
    return rect.top + window.scrollY;
5✔
203
};
204

205
export const embedEventStatus = {
35✔
206
    START: 'start',
207
    END: 'end',
208
};
209

210
export const setAttributes = (
35✔
211
    element: HTMLElement,
212
    attributes: { [key: string]: string | number | boolean },
213
): void => {
214
    Object.keys(attributes).forEach((key) => {
401✔
215
        element.setAttribute(key, attributes[key].toString());
21✔
216
    });
217
};
218

219
const isCloudRelease = (version: string) => version.endsWith('.cl');
35✔
220

221
/* For Search Embed: ReleaseVersionInBeta */
222
export const checkReleaseVersionInBeta = (
35✔
223
    releaseVersion: string,
224
    suppressBetaWarning: boolean,
225
): boolean => {
226
    if (releaseVersion !== '' && !isCloudRelease(releaseVersion)) {
110✔
227
        const splittedReleaseVersion = releaseVersion.split('.');
34✔
228
        const majorVersion = Number(splittedReleaseVersion[0]);
34✔
229
        const isBetaVersion = majorVersion < 8;
34✔
230
        return !suppressBetaWarning && isBetaVersion;
34✔
231
    }
232
    return false;
76✔
233
};
234

235
export const getCustomisations = (
35✔
236
    embedConfig: EmbedConfig,
237
    viewConfig: AllEmbedViewConfig,
238
): CustomisationsInterface => {
239
    const customizationsFromViewConfig = viewConfig.customizations;
20✔
240
    const customizationsFromEmbedConfig = embedConfig.customizations
20✔
241
        || ((embedConfig as any).customisations as CustomisationsInterface);
242

243
    const customizations: CustomisationsInterface = {
20✔
244
        style: {
245
            ...customizationsFromEmbedConfig?.style,
60✔
246
            ...customizationsFromViewConfig?.style,
60✔
247
            customCSS: {
248
                ...customizationsFromEmbedConfig?.style?.customCSS,
120✔
249
                ...customizationsFromViewConfig?.style?.customCSS,
120✔
250
            },
251
            customCSSUrl:
252
                customizationsFromViewConfig?.style?.customCSSUrl
160✔
253
                || customizationsFromEmbedConfig?.style?.customCSSUrl,
120✔
254
        },
255
        content: {
256
            ...customizationsFromEmbedConfig?.content,
60✔
257
            ...customizationsFromViewConfig?.content,
60✔
258
        },
259
    };
260
    return customizations;
20✔
261
};
262

263
export const getRuntimeFilters = (runtimefilters: any) => getFilterQuery(runtimefilters || []);
35!
264

265
/**
266
 * Gets a reference to the DOM node given
267
 * a selector.
268
 * @param domSelector
269
 */
270
export function getDOMNode(domSelector: DOMSelector): HTMLElement {
35✔
271
    return typeof domSelector === 'string' ? document.querySelector(domSelector) : domSelector;
467✔
272
}
273

274
export const deepMerge = (target: any, source: any) => merge(target, source);
35✔
275

276
export const getOperationNameFromQuery = (query: string) => {
35✔
277
    const regex = /(?:query|mutation)\s+(\w+)/;
88✔
278
    const matches = query.match(regex);
88✔
279
    return matches?.[1];
88!
280
};
281

282
/**
283
 *
284
 * @param obj
285
 */
286
export function removeTypename(obj: any) {
35✔
287
    if (!obj || typeof obj !== 'object') return obj;
47✔
288

289

290
    for (const key in obj) {
36✔
291
        if (key === '__typename') {
90✔
292
            delete obj[key];
2✔
293
        } else if (typeof obj[key] === 'object') {
88✔
294
            removeTypename(obj[key]);
18✔
295
        }
296
    }
297
    return obj;
36✔
298
}
299

300
/**
301
 * Sets the specified style properties on an HTML element.
302
 * @param {HTMLElement} element - The HTML element to which the styles should be applied.
303
 * @param {Partial<CSSStyleDeclaration>} styleProperties - An object containing style
304
 * property names and their values.
305
 * @example
306
 * // Apply styles to an element
307
 * const element = document.getElementById('myElement');
308
 * const styles = {
309
 *   backgroundColor: 'red',
310
 *   fontSize: '16px',
311
 * };
312
 * setStyleProperties(element, styles);
313
 */
314
export const setStyleProperties = (
35✔
315
    element: HTMLElement,
316
    styleProperties: Partial<CSSStyleDeclaration>,
317
): void => {
318
    if (!element?.style) return;
37✔
319
    Object.keys(styleProperties).forEach((styleProperty) => {
36✔
320
        const styleKey = styleProperty as keyof CSSStyleDeclaration;
124✔
321
        const value = styleProperties[styleKey];
124✔
322
        if (value !== undefined) {
124✔
323
            (element.style as any)[styleKey] = value.toString();
124✔
324
        }
325
    });
326
};
327
/**
328
 * Removes specified style properties from an HTML element.
329
 * @param {HTMLElement} element - The HTML element from which the styles should be removed.
330
 * @param {string[]} styleProperties - An array of style property names to be removed.
331
 * @example
332
 * // Remove styles from an element
333
 * const element = document.getElementById('myElement');
334
 * element.style.backgroundColor = 'red';
335
 * const propertiesToRemove = ['backgroundColor'];
336
 * removeStyleProperties(element, propertiesToRemove);
337
 */
338
export const removeStyleProperties = (element: HTMLElement, styleProperties: string[]): void => {
35✔
339
    if (!element?.style) return;
9✔
340
    styleProperties.forEach((styleProperty) => {
8✔
341
        element.style.removeProperty(styleProperty);
22✔
342
    });
343
};
344

345
export const isUndefined = (value: any): boolean => value === undefined;
309✔
346

347
// Return if the value is a string, double or boolean.
348
export const getTypeFromValue = (value: any): [string, string] => {
35✔
349
    if (typeof value === 'string') {
9✔
350
        return ['char', 'string'];
1✔
351
    }
352
    if (typeof value === 'number') {
8✔
353
        return ['double', 'double'];
2✔
354
    }
355
    if (typeof value === 'boolean') {
6✔
356
        return ['boolean', 'boolean'];
2✔
357
    }
358
    return ['', ''];
4✔
359
};
360

361
const sdkWindowKey = '_tsEmbedSDK' as any;
35✔
362

363
/**
364
 * Stores a value in the global `window` object under the `_tsEmbedSDK` namespace.
365
 * @param key - The key under which the value will be stored.
366
 * @param value - The value to store.
367
 * @param options - Additional options.
368
 * @param options.ignoreIfAlreadyExists - Does not set if value for key is set.
369
 *
370
 * @returns The stored value.
371
 *
372
 * @version SDK: 1.36.2 | ThoughtSpot: *
373
 */
374
export function storeValueInWindow<T>(
35✔
375
    key: string,
376
    value: T,
377
    options: { ignoreIfAlreadyExists?: boolean } = {},
459✔
378
): T {
379
    if (!window[sdkWindowKey]) {
479✔
380
        (window as any)[sdkWindowKey] = {};
22✔
381
    }
382

383
    if (options.ignoreIfAlreadyExists && key in (window as any)[sdkWindowKey]) {
479✔
384
        return (window as any)[sdkWindowKey][key];
1✔
385
    }
386

387
    (window as any)[sdkWindowKey][key] = value;
478✔
388
    return value;
478✔
389
}
390

391
/**
392
 * Retrieves a stored value from the global `window` object under the `_tsEmbedSDK` namespace.
393
 * @param key - The key whose value needs to be retrieved.
394
 * @returns The stored value or `undefined` if the key is not found.
395
 */
396
export const getValueFromWindow = <T = any>
35✔
397
    (key: string): T => (window as any)?.[sdkWindowKey]?.[key];
3,257!
398

399
/**
400
 * Check if an array includes a string value
401
 * @param arr - The array to check
402
 * @param key - The string to search for
403
 * @returns boolean indicating if the string is found in the array
404
 */
405
export const arrayIncludesString = (arr: readonly unknown[], key: string): boolean => {
35✔
406
    return arr.some(item => typeof item === 'string' && item === key);
397✔
407
};
408

409
/**
410
 * Resets the key if it exists in the `window` object under the `_tsEmbedSDK` key.
411
 * Returns true if the key was reset, false otherwise.
412
 * @param key - Key to reset
413
 * @returns - boolean indicating if the key was reset
414
 */
415
export function resetValueFromWindow(key: string): boolean {
35✔
416
    if (key in window[sdkWindowKey]) {
3✔
417
        delete (window as any)[sdkWindowKey][key];
3✔
418
        return true;
3✔
419
    }
420
    return false;
×
421
}
422

423
/**
424
 * Check if the document is currently in fullscreen mode
425
 */
426
const isInFullscreen = (): boolean => {
35✔
427
    return !!(
6✔
428
        document.fullscreenElement ||
15✔
429
        (document as any).webkitFullscreenElement ||
430
        (document as any).mozFullScreenElement ||
431
        (document as any).msFullscreenElement
432
    );
433
};
434

435
/**
436
 * Handle Present HostEvent by entering fullscreen mode
437
 * @param iframe The iframe element to make fullscreen
438
 */
439
export const handlePresentEvent = async (iframe: HTMLIFrameElement): Promise<void> => {
35✔
440
    if (isInFullscreen()) {
3✔
441
        return; // Already in fullscreen
1✔
442
    }
443

444
    // Browser-specific methods to enter fullscreen mode
445
    const fullscreenMethods = [
2✔
446
        'requestFullscreen',      // Standard API
447
        'webkitRequestFullscreen', // WebKit browsers
448
        'mozRequestFullScreen',   // Firefox
449
        'msRequestFullscreen'     // IE/Edge
450
    ];
451

452
    for (const method of fullscreenMethods) {
2✔
453
        if (typeof (iframe as any)[method] === 'function') {
5✔
454
            try {
1✔
455
                const result = (iframe as any)[method]();
1✔
456
                await Promise.resolve(result);
1✔
457
                return;
1✔
458
            } catch (error) {
459
                logger.warn(`Failed to enter fullscreen using ${method}:`, error);
×
460
            }
461
        }
462
    }
463

464
    logger.error('Fullscreen API is not supported by this browser.');
1✔
465
};
466

467
/**
468
 * Handle ExitPresentMode EmbedEvent by exiting fullscreen mode
469
 */
470
export const handleExitPresentMode = async (): Promise<void> => {
35✔
471
    if (!isInFullscreen()) {
3✔
472
        return; // Not in fullscreen
1✔
473
    }
474

475
    const exitFullscreenMethods = [
2✔
476
        'exitFullscreen',        // Standard API
477
        'webkitExitFullscreen',  // WebKit browsers
478
        'mozCancelFullScreen',   // Firefox
479
        'msExitFullscreen'       // IE/Edge
480
    ];
481

482
    // Try each method until one works
483
    for (const method of exitFullscreenMethods) {
2✔
484
        if (typeof (document as any)[method] === 'function') {
5✔
485
            try {
1✔
486
                const result = (document as any)[method]();
1✔
487
                await Promise.resolve(result);
1✔
488
                return;
1✔
489
            } catch (error) {
490
                logger.warn(`Failed to exit fullscreen using ${method}:`, error);
×
491
            }
492
        }
493
    }
494

495
    logger.warn('Exit fullscreen API is not supported by this browser.');
1✔
496
};
497

498
export const calculateVisibleElementData = (element: HTMLElement) => {
35✔
499
    const rect = element.getBoundingClientRect();
19✔
500

501
    const windowHeight = window.innerHeight;
19✔
502
    const windowWidth = window.innerWidth;
19✔
503

504
    const frameRelativeTop = Math.max(rect.top, 0);
19✔
505
    const frameRelativeLeft = Math.max(rect.left, 0);
19✔
506

507
    const frameRelativeBottom = Math.min(windowHeight, rect.bottom);
19✔
508
    const frameRelativeRight = Math.min(windowWidth, rect.right);
19✔
509

510
    const data = {
19✔
511
        top: Math.max(0, rect.top * -1),
512
        height: Math.max(0, frameRelativeBottom - frameRelativeTop),
513
        left: Math.max(0, rect.left * -1),
514
        width: Math.max(0, frameRelativeRight - frameRelativeLeft),
515
    };
516

517
    return data;
19✔
518
}
519

520
/**
521
 * Replaces placeholders in a template string with provided values.
522
 * Placeholders should be in the format {key}.
523
 * @param template - The template string with placeholders
524
 * @param values - An object containing key-value pairs to replace placeholders
525
 * @returns The template string with placeholders replaced
526
 * @example
527
 * formatTemplate('Hello {name}, you are {age} years old', { name: 'John', age: 30 })
528
 * // Returns: 'Hello John, you are 30 years old'
529
 *
530
 * formatTemplate('Expected {type}, but received {actual}', { type: 'string', actual: 'number' })
531
 * // Returns: 'Expected string, but received number'
532
 */
533
export const formatTemplate = (template: string, values: Record<string, any>): string => {
35✔
534
    // This regex /\{(\w+)\}/g finds all placeholders in the format {word} 
535
    // and captures the word inside the braces for replacement.
536
    return template.replace(/\{(\w+)\}/g, (match, key) => {
7✔
537
        return values[key] !== undefined ? String(values[key]) : match;
10✔
538
    });
539
};
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