@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
1,053 lines • 45.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("./utils");
const types_1 = require("./types");
const logger_1 = require("./utils/logger");
const errors_1 = require("./errors");
// Mock logger
jest.mock('./utils/logger', () => ({
logger: {
warn: jest.fn(),
error: jest.fn(),
},
}));
describe('unit test for utils', () => {
test('getQueryParamString', () => {
expect((0, utils_1.getQueryParamString)({
foo: 'bar',
baz: '42',
})).toBe('foo=bar&baz=42');
expect((0, utils_1.getQueryParamString)({})).toBe(null);
// should not add undefined params
expect((0, utils_1.getQueryParamString)({
foo: undefined,
bar: 'baz',
})).toBe('bar=baz');
});
test('getFilterQuery should encode URL params', () => {
expect((0, utils_1.getFilterQuery)([])).toBe(null);
expect((0, utils_1.getFilterQuery)([
{
columnName: 'foo+foo',
operator: types_1.RuntimeFilterOp.NE,
values: ['bar+'],
},
])).toBe('col1=foo%2Bfoo&op1=NE&val1=bar%2B');
});
test('getFilterQuery', () => {
expect((0, utils_1.getFilterQuery)([])).toBe(null);
expect((0, utils_1.getFilterQuery)([
{
columnName: 'foo',
operator: types_1.RuntimeFilterOp.NE,
values: ['bar'],
},
])).toBe('col1=foo&op1=NE&val1=bar');
const filters = [
{
columnName: 'foo',
operator: types_1.RuntimeFilterOp.EQ,
values: [42],
},
{
columnName: 'bar',
operator: types_1.RuntimeFilterOp.BW_INC,
values: [1, 10],
},
{
columnName: 'baz',
operator: types_1.RuntimeFilterOp.CONTAINS,
values: ['abc'],
},
];
expect((0, utils_1.getFilterQuery)(filters)).toBe('col1=foo&op1=EQ&val1=42&col2=bar&op2=BW_INC&val2=1&val2=10&col3=baz&op3=CONTAINS&val3=abc');
});
test('getParameterOverride', () => {
expect((0, utils_1.getRuntimeParameters)([])).toBe(null);
expect((0, utils_1.getRuntimeParameters)([
{
name: 'foo',
value: 'bar',
},
])).toBe('param1=foo¶mVal1=bar');
const params = [
{
name: 'foo',
value: 42,
},
{
name: 'bar',
value: 'abc',
},
{
name: 'baz',
value: true,
},
];
expect((0, utils_1.getRuntimeParameters)(params)).toBe('param1=foo¶mVal1=42¶m2=bar¶mVal2=abc¶m3=baz¶mVal3=true');
});
test('getCssDimension', () => {
expect((0, utils_1.getCssDimension)(100)).toBe('100px');
expect((0, utils_1.getCssDimension)('100%')).toBe('100%');
expect((0, utils_1.getCssDimension)('100px')).toBe('100px');
expect((0, utils_1.getCssDimension)(null)).toBe(null);
});
test('appendToUrlHash', () => {
expect((0, utils_1.appendToUrlHash)('http://myhost:3000', 'hashFrag')).toBe('http://myhost:3000#?tsSSOMarker=hashFrag');
expect((0, utils_1.appendToUrlHash)('http://xyz.com/#foo', 'bar')).toBe('http://xyz.com/#foo?tsSSOMarker=bar');
});
describe('getRedirectURL', () => {
let originalLocation;
beforeEach(() => {
originalLocation = window.location;
});
afterEach(() => {
window.location.hash = '';
});
test('Should return correct value when path is undefined', () => {
expect((0, utils_1.getRedirectUrl)('http://myhost:3000', 'hashFrag')).toBe('http://myhost:3000#?tsSSOMarker=hashFrag');
expect((0, utils_1.getRedirectUrl)('http://xyz.com/#foo', 'bar')).toBe('http://xyz.com/#foo?tsSSOMarker=bar');
});
test('Should return correct value when path is set', () => {
expect((0, utils_1.getRedirectUrl)('http://myhost:3000/', 'hashFrag', '/bar')).toBe('http://localhost/bar#?tsSSOMarker=hashFrag');
expect((0, utils_1.getRedirectUrl)('http://myhost:3000/#/foo', 'hashFrag', '#/bar')).toBe('http://localhost/#/bar?tsSSOMarker=hashFrag');
});
});
test('getEncodedQueryParamsString', () => {
expect((0, utils_1.getEncodedQueryParamsString)('')).toBe('');
expect((0, utils_1.getEncodedQueryParamsString)('test')).toBe('dGVzdA');
});
test('when ReleaseVersion is empty ', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('', false)).toBe(false);
});
test('when ReleaseVersion is 7.0.1.cl ', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('7.0.1.cl', false)).toBe(false);
});
test('when cluster has dev version', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('dev', false)).toBe(false);
});
test('when cluster is above 8.4.0.cl-11 software version', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('8.4.0.cl-117', false)).toBe(false);
});
test('when cluster is bellow 8.0.0.sw software version', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('7.2.1.sw', false)).toBe(true);
});
test('when suppressBetaWarning is true and ReleaseVersion is 7.0.1', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('7.0.1', true)).toBe(false);
});
test('when suppressBetaWarning is false ReleaseVersion is 7.0.1', () => {
expect((0, utils_1.checkReleaseVersionInBeta)('7.0.1', false)).toBe(true);
});
test('removeTypename should removed __typename', () => {
const input = {
test: 'test',
__typename: 'should be removed',
obj: {
test: 'test',
__typename: 'should be removed',
},
};
const result = (0, utils_1.removeTypename)(input);
const expectedResult = {
test: 'test',
obj: {
test: 'test',
},
};
expect(result).toEqual(expectedResult);
});
describe('validate removeStyleProperties', () => {
it('should remove specified style properties from an HTML element', () => {
const element = document.createElement('div');
element.style.backgroundColor = 'blue';
element.style.fontSize = '14px';
const propertiesToRemove = ['background-color', 'font-size'];
(0, utils_1.removeStyleProperties)(element, propertiesToRemove);
expect(element.style.backgroundColor).toBe('');
expect(element.style.fontSize).toBe('');
});
it('should handle undefined param', () => {
expect(() => {
(0, utils_1.removeStyleProperties)(undefined, []);
}).not.toThrow();
});
it('should handle removing non-existent style properties', () => {
const element = document.createElement('div');
element.style.backgroundColor = 'blue';
element.style.fontSize = '14px';
const propertiesToRemove = ['color', 'border'];
(0, utils_1.removeStyleProperties)(element, propertiesToRemove);
expect(element.style.backgroundColor).toBe('blue');
expect(element.style.fontSize).toBe('14px');
});
});
describe('validate setStyleProperties', () => {
it('should set style properties on an HTML element', () => {
const element = document.createElement('div');
const styles = {
backgroundColor: 'red',
fontSize: '16px',
};
(0, utils_1.setStyleProperties)(element, styles);
expect(element.style.backgroundColor).toBe('red');
expect(element.style.fontSize).toBe('16px');
});
it('should handle undefined param', () => {
// should not throw an error
expect(() => {
(0, utils_1.setStyleProperties)(undefined, {});
}).not.toThrow();
});
});
test('isUndefined', () => {
expect((0, utils_1.isUndefined)(undefined)).toBe(true);
expect((0, utils_1.isUndefined)({})).toBe(false);
expect((0, utils_1.isUndefined)(null)).toBe(false);
expect((0, utils_1.isUndefined)('')).toBe(false);
expect((0, utils_1.isUndefined)(0)).toBe(false);
});
test('removeTypename should handle edge cases', () => {
expect((0, utils_1.removeTypename)(null)).toBe(null);
expect((0, utils_1.removeTypename)(undefined)).toBe(undefined);
expect((0, utils_1.removeTypename)('string')).toBe('string');
expect((0, utils_1.removeTypename)(123)).toBe(123);
});
test('getTypeFromValue should return correct types', () => {
expect((0, utils_1.getTypeFromValue)('test')).toEqual(['char', 'string']);
expect((0, utils_1.getTypeFromValue)(123)).toEqual(['double', 'double']);
expect((0, utils_1.getTypeFromValue)(true)).toEqual(['boolean', 'boolean']);
expect((0, utils_1.getTypeFromValue)(false)).toEqual(['boolean', 'boolean']);
expect((0, utils_1.getTypeFromValue)(null)).toEqual(['', '']);
expect((0, utils_1.getTypeFromValue)(undefined)).toEqual(['', '']);
expect((0, utils_1.getTypeFromValue)({})).toEqual(['', '']);
expect((0, utils_1.getTypeFromValue)([])).toEqual(['', '']);
});
describe('getValueFromWindow and storeValueInWindow', () => {
test('Store and retrieve', () => {
(0, utils_1.storeValueInWindow)('test', 'testValue');
expect((0, utils_1.getValueFromWindow)('test')).toBe('testValue');
});
test('Object should be set if not', () => {
window._tsEmbedSDK = null;
(0, utils_1.storeValueInWindow)('test', 'testValue');
expect((0, utils_1.getValueFromWindow)('test')).toBe('testValue');
});
test('Return undefined if key is not found', () => {
expect((0, utils_1.getValueFromWindow)('notFound')).toBe(undefined);
});
test('Store with ignoreIfAlreadyExists option', () => {
(0, utils_1.storeValueInWindow)('test2', 'firstValue');
const result = (0, utils_1.storeValueInWindow)('test2', 'secondValue', { ignoreIfAlreadyExists: true });
expect(result).toBe('firstValue');
expect((0, utils_1.getValueFromWindow)('test2')).toBe('firstValue');
});
});
});
describe('Fullscreen Utility Functions', () => {
let originalExitFullscreen;
let mockIframe;
beforeEach(() => {
jest.clearAllMocks();
// Store and mock exitFullscreen
originalExitFullscreen = document.exitFullscreen;
document.exitFullscreen = jest.fn();
// Mock iframe
mockIframe = {
requestFullscreen: jest.fn(),
};
// Mock not in fullscreen initially
Object.defineProperty(document, 'fullscreenElement', {
writable: true,
value: null,
});
});
afterEach(() => {
// Restore original method
document.exitFullscreen = originalExitFullscreen;
});
describe('handlePresentEvent', () => {
it('should enter fullscreen when iframe is provided', () => {
const mockPromise = Promise.resolve();
mockIframe.requestFullscreen.mockReturnValue(mockPromise);
(0, utils_1.handlePresentEvent)(mockIframe);
expect(mockIframe.requestFullscreen).toHaveBeenCalled();
expect(logger_1.logger.error).not.toHaveBeenCalled();
});
it('should log error when fullscreen API is not supported', () => {
delete mockIframe.requestFullscreen;
(0, utils_1.handlePresentEvent)(mockIframe);
expect(logger_1.logger.error).toHaveBeenCalledWith('Fullscreen API is not supported by this browser.');
});
it('should catch error and log warning when requestFullscreen throws (covers line 506)', async () => {
// Covers line 506: catch block inside fullscreen method attempt
mockIframe.requestFullscreen.mockRejectedValue(new Error('permission denied'));
await (0, utils_1.handlePresentEvent)(mockIframe);
expect(logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to enter fullscreen'), expect.any(Error));
});
it('should not attempt fullscreen when already in fullscreen mode', () => {
Object.defineProperty(document, 'fullscreenElement', {
writable: true,
value: mockIframe,
});
(0, utils_1.handlePresentEvent)(mockIframe);
expect(mockIframe.requestFullscreen).not.toHaveBeenCalled();
expect(logger_1.logger.error).not.toHaveBeenCalled();
});
});
describe('handleExitPresentMode', () => {
beforeEach(() => {
// Mock being in fullscreen
Object.defineProperty(document, 'fullscreenElement', {
writable: true,
value: document.createElement('iframe'),
});
});
it('should exit fullscreen when in fullscreen mode', () => {
const mockPromise = Promise.resolve();
document.exitFullscreen.mockReturnValue(mockPromise);
(0, utils_1.handleExitPresentMode)();
expect(document.exitFullscreen).toHaveBeenCalled();
expect(logger_1.logger.warn).not.toHaveBeenCalled();
});
it('should not attempt to exit when not in fullscreen mode', () => {
Object.defineProperty(document, 'fullscreenElement', {
writable: true,
value: null,
});
(0, utils_1.handleExitPresentMode)();
expect(document.exitFullscreen).not.toHaveBeenCalled();
expect(logger_1.logger.warn).not.toHaveBeenCalled();
});
it('should log warning when exit fullscreen API is not supported', () => {
// Mock being in fullscreen but no exit methods available
document.exitFullscreen = undefined;
(0, utils_1.handleExitPresentMode)();
expect(logger_1.logger.warn).toHaveBeenCalledWith('Exit fullscreen API is not supported by this browser.');
});
});
describe('arrayIncludesString', () => {
it('should return true when string is found in array', () => {
const arr = ['test', 'example', 'value'];
expect((0, utils_1.arrayIncludesString)(arr, 'test')).toBe(true);
expect((0, utils_1.arrayIncludesString)(arr, 'example')).toBe(true);
expect((0, utils_1.arrayIncludesString)(arr, 'value')).toBe(true);
});
it('should return false when string is not found in array', () => {
const arr = ['test', 'example', 'value'];
expect((0, utils_1.arrayIncludesString)(arr, 'notfound')).toBe(false);
expect((0, utils_1.arrayIncludesString)(arr, '')).toBe(false);
});
it('should handle empty array', () => {
const arr = [];
expect((0, utils_1.arrayIncludesString)(arr, 'test')).toBe(false);
});
it('should handle array with non-string values', () => {
const arr = ['test', 123, true, 'value'];
expect((0, utils_1.arrayIncludesString)(arr, 'test')).toBe(true);
expect((0, utils_1.arrayIncludesString)(arr, 'value')).toBe(true);
expect((0, utils_1.arrayIncludesString)(arr, '123')).toBe(false); // string '123' not found
});
it('should be case sensitive', () => {
const arr = ['Test', 'Example', 'Value'];
expect((0, utils_1.arrayIncludesString)(arr, 'test')).toBe(false);
expect((0, utils_1.arrayIncludesString)(arr, 'Test')).toBe(true);
});
});
});
describe('calculateVisibleElementData', () => {
let mockElement;
let originalInnerHeight;
let originalInnerWidth;
beforeEach(() => {
// Store original window dimensions
originalInnerHeight = window.innerHeight;
originalInnerWidth = window.innerWidth;
// Mock window dimensions
Object.defineProperty(window, 'innerHeight', {
writable: true,
configurable: true,
value: 800,
});
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
value: 1200,
});
// Create mock element
mockElement = document.createElement('div');
});
afterEach(() => {
// Restore original window dimensions
Object.defineProperty(window, 'innerHeight', {
value: originalInnerHeight,
});
Object.defineProperty(window, 'innerWidth', {
value: originalInnerWidth,
});
});
it('should calculate data for fully visible element', () => {
// Mock getBoundingClientRect for element fully within viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 150,
bottom: 300,
right: 400,
width: 250,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 0,
height: 200,
left: 0,
width: 250, // Full width visible
});
});
it('should return zero dimensions when element is missing', () => {
expect((0, utils_1.calculateVisibleElementData)(null)).toEqual({
top: 0,
height: 0,
left: 0,
width: 0,
});
});
it('should calculate data for element clipped from top', () => {
// Mock getBoundingClientRect for element partially above viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: -50,
left: 100,
bottom: 150,
right: 400,
width: 300,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 50,
height: 150,
left: 0,
width: 300, // Full width visible
});
});
it('should calculate data for element clipped from left', () => {
// Mock getBoundingClientRect for element partially left of viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: -80,
bottom: 300,
right: 200,
width: 280,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 0,
height: 200,
left: 80,
width: 200, // 200px visible width (0 to 200)
});
});
it('should calculate data for element clipped from bottom', () => {
// Mock getBoundingClientRect for element extending below viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 600,
left: 100,
bottom: 950,
right: 400,
width: 300,
height: 350,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 0,
height: 200,
left: 0,
width: 300, // Full width visible
});
});
it('should calculate data for element clipped from right', () => {
// Mock getBoundingClientRect for element extending beyond right edge
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 1000,
bottom: 300,
right: 1400,
width: 400,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 0,
height: 200,
left: 0,
width: 200, // Only 200px visible width (1000 to 1200)
});
});
it('should calculate data for element clipped from multiple sides', () => {
// Mock getBoundingClientRect for element clipped from top and left
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: -100,
left: -50,
bottom: 200,
right: 300,
width: 350,
height: 300,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 100,
height: 200,
left: 50,
width: 300, // 300px visible width (0 to 300)
});
});
it('should handle element completely outside viewport (above)', () => {
// Mock getBoundingClientRect for element completely above viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: -300,
left: 100,
bottom: -100,
right: 400,
width: 300,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 300,
height: 0,
left: 0,
width: 300, // Full width would be visible if in viewport
});
});
it('should handle element completely outside viewport (left)', () => {
// Mock getBoundingClientRect for element completely left of viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: -400,
bottom: 300,
right: -100,
width: 300,
height: 200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 0,
height: 200,
left: 400,
width: 0, // No visible width (min(1200, -100) - max(-400, 0) = -100 - 0 = -100, but clamped)
});
});
it('should handle element larger than viewport', () => {
// Mock getBoundingClientRect for element larger than viewport
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: -200,
left: -300,
bottom: 1000,
right: 1500,
width: 1800,
height: 1200,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 200,
height: 800,
left: 300,
width: 1200, // Visible width equals window width
});
});
it('should handle element exactly at viewport boundaries', () => {
// Mock getBoundingClientRect for element at exact viewport boundaries
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 0,
left: 0,
bottom: 800,
right: 1200,
width: 1200,
height: 800,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement);
expect(result).toEqual({
top: 0,
height: 800,
left: 0,
width: 1200, // Full viewport width
});
});
it('should calculate data clipped by a scrollable parent', () => {
const scrollContainer = document.createElement('div');
scrollContainer.style.overflow = 'auto';
scrollContainer.appendChild(mockElement);
jest.spyOn(scrollContainer, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 50,
bottom: 600,
right: 650,
width: 600,
height: 500,
});
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: -100,
left: 50,
bottom: 900,
right: 650,
width: 600,
height: 1000,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 200,
height: 500,
left: 0,
width: 600,
});
});
it('should calculate data clipped by a non-scroll clipping parent', () => {
const clippingContainer = document.createElement('div');
clippingContainer.style.overflow = 'hidden';
clippingContainer.appendChild(mockElement);
jest.spyOn(clippingContainer, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 100,
bottom: 500,
right: 500,
width: 400,
height: 400,
});
jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
top: 50,
left: 50,
bottom: 700,
right: 700,
width: 650,
height: 650,
});
const result = (0, utils_1.calculateVisibleElementData)(mockElement, true);
expect(result).toEqual({
top: 50,
height: 400,
left: 50,
width: 400,
});
});
});
describe('getScrollableAncestors', () => {
it('should return an empty list when element is missing', () => {
expect((0, utils_1.getScrollableAncestors)(null)).toEqual([]);
});
it('should find scrollable ancestors inside a shadow root', () => {
const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const scrollContainer = document.createElement('div');
scrollContainer.style.overflow = 'auto';
const embedTarget = document.createElement('div');
const iframe = document.createElement('iframe');
shadow.appendChild(scrollContainer);
scrollContainer.appendChild(embedTarget);
embedTarget.appendChild(iframe);
expect((0, utils_1.getScrollableAncestors)(iframe)).toEqual([scrollContainer]);
host.remove();
});
it('should ignore ancestors when computed style is unavailable', () => {
const parent = document.createElement('div');
const iframe = document.createElement('iframe');
parent.appendChild(iframe);
const getComputedStyleSpy = jest
.spyOn(window, 'getComputedStyle')
.mockReturnValue(null);
expect((0, utils_1.getScrollableAncestors)(iframe)).toEqual([]);
getComputedStyleSpy.mockRestore();
});
});
describe('getClippingAncestors', () => {
it('should return an empty list when element is missing', () => {
expect((0, utils_1.getClippingAncestors)(null)).toEqual([]);
});
it('should include scrollable and non-scroll clipping ancestors', () => {
const scrollContainer = document.createElement('div');
scrollContainer.style.overflow = 'auto';
const clippingContainer = document.createElement('div');
clippingContainer.style.overflow = 'hidden';
const iframe = document.createElement('iframe');
scrollContainer.appendChild(clippingContainer);
clippingContainer.appendChild(iframe);
expect((0, utils_1.getClippingAncestors)(iframe)).toEqual([clippingContainer, scrollContainer]);
});
});
describe('querySelectorAcrossShadowRoot', () => {
it('should resolve a selector from the light DOM document', () => {
const el = document.createElement('div');
el.id = 'light-dom-target';
document.body.appendChild(el);
expect((0, utils_1.querySelectorAcrossShadowRoot)('#light-dom-target')).toBe(el);
el.remove();
});
it('should resolve a selector inside the reference node shadow root when the document misses', () => {
const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const container = document.createElement('div');
container.id = 'shadow-target';
const reference = document.createElement('div');
shadow.appendChild(container);
shadow.appendChild(reference);
// document.querySelector cannot see into the shadow root.
expect(document.querySelector('#shadow-target')).toBeNull();
expect((0, utils_1.querySelectorAcrossShadowRoot)('#shadow-target', reference)).toBe(container);
host.remove();
});
it('should prefer the document match over the shadow root', () => {
const lightEl = document.createElement('div');
lightEl.id = 'shared-id';
document.body.appendChild(lightEl);
const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const shadowEl = document.createElement('div');
shadowEl.id = 'shared-id';
const reference = document.createElement('div');
shadow.appendChild(shadowEl);
shadow.appendChild(reference);
expect((0, utils_1.querySelectorAcrossShadowRoot)('#shared-id', reference)).toBe(lightEl);
lightEl.remove();
host.remove();
});
it('should return null when nothing matches and the reference node is in the light DOM', () => {
const reference = document.createElement('div');
document.body.appendChild(reference);
expect((0, utils_1.querySelectorAcrossShadowRoot)('#does-not-exist', reference)).toBeNull();
reference.remove();
});
it('should return null when no reference node is provided and the document misses', () => {
expect((0, utils_1.querySelectorAcrossShadowRoot)('#does-not-exist')).toBeNull();
});
});
describe('getEffectiveClippingAncestors', () => {
it('should return an empty list when element is missing', () => {
expect((0, utils_1.getEffectiveClippingAncestors)(null)).toEqual([]);
});
it('should ignore overflow ancestors that do not clip the element', () => {
const clippingContainer = document.createElement('div');
clippingContainer.style.overflow = 'hidden';
const iframe = document.createElement('iframe');
clippingContainer.appendChild(iframe);
jest.spyOn(clippingContainer, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 100,
bottom: 700,
right: 700,
width: 600,
height: 600,
});
jest.spyOn(iframe, 'getBoundingClientRect').mockReturnValue({
top: 200,
left: 200,
bottom: 400,
right: 400,
width: 200,
height: 200,
});
expect((0, utils_1.getEffectiveClippingAncestors)(iframe)).toEqual([]);
});
it('should include overflow ancestors that clip the element', () => {
const clippingContainer = document.createElement('div');
clippingContainer.style.overflow = 'hidden';
const iframe = document.createElement('iframe');
clippingContainer.appendChild(iframe);
jest.spyOn(clippingContainer, 'getBoundingClientRect').mockReturnValue({
top: 100,
left: 100,
bottom: 500,
right: 500,
width: 400,
height: 400,
});
jest.spyOn(iframe, 'getBoundingClientRect').mockReturnValue({
top: 50,
left: 50,
bottom: 700,
right: 700,
width: 650,
height: 650,
});
expect((0, utils_1.getEffectiveClippingAncestors)(iframe)).toEqual([clippingContainer]);
});
});
describe('formatTemplate', () => {
it('should replace placeholders with provided values', () => {
expect((0, utils_1.formatTemplate)('Hello {name}, you are {age} years old', { name: 'John', age: 30 })).toBe('Hello John, you are 30 years old');
expect((0, utils_1.formatTemplate)('Expected {type}, but received {actual}', {
type: 'string',
actual: 'number',
})).toBe('Expected string, but received number');
expect((0, utils_1.formatTemplate)('Hello {name}, you are {age} years old', { name: 'John' })).toBe('Hello John, you are {age} years old');
});
});
describe('isValidCssMargin', () => {
it('should return true for valid CSS margin values', () => {
expect((0, utils_1.isValidCssMargin)('10px')).toBe(true);
expect((0, utils_1.isValidCssMargin)('0px')).toBe(true);
expect((0, utils_1.isValidCssMargin)('20%')).toBe(true);
expect((0, utils_1.isValidCssMargin)('0')).toBe(true);
});
it('should return false for invalid CSS margin values', () => {
expect((0, utils_1.isValidCssMargin)('')).toBe(false);
expect((0, utils_1.isValidCssMargin)(' ')).toBe(false);
expect((0, utils_1.isValidCssMargin)('invalid')).toBe(false);
expect((0, utils_1.isValidCssMargin)('10')).toBe(false); // missing unit
});
it('should return false and log error when value is not a string (non-string type)', () => {
// Covers line 147-148: typeof value !== 'string' branch
expect((0, utils_1.isValidCssMargin)(42)).toBe(false);
expect(logger_1.logger.error).toHaveBeenCalledWith('Please provide a valid lazyLoadingMargin value (e.g., "10px")');
});
it('should return false when more than 4 space-separated parts are given', () => {
// Covers line 157-158: parts.length > 4 branch
expect((0, utils_1.isValidCssMargin)('10px 20px 30px 40px 50px')).toBe(false);
expect(logger_1.logger.error).toHaveBeenCalledWith('Please provide a valid lazyLoadingMargin value (e.g., "10px")');
});
it('should return true for valid multi-part margin (up to 4 parts)', () => {
expect((0, utils_1.isValidCssMargin)('10px 20px')).toBe(true);
expect((0, utils_1.isValidCssMargin)('10px 20px 30px 40px')).toBe(true);
expect((0, utils_1.isValidCssMargin)('auto')).toBe(true);
});
});
describe('getValueFromWindow and storeValueInWindow', () => {
describe('SSR environment handling', () => {
let originalWindow;
beforeEach(() => {
originalWindow = global.window;
});
afterEach(() => {
global.window = originalWindow;
});
test('storeValueInWindow should log error in SSR environment', () => {
delete global.window;
const result = (0, utils_1.storeValueInWindow)('testKey', 'testValue');
expect(logger_1.logger.error).toHaveBeenCalledWith(errors_1.ERROR_MESSAGE.SSR_ENVIRONMENT_ERROR);
expect(result).toBe('testValue');
});
test('getValueFromWindow should log error in SSR environment', () => {
delete global.window;
const result = (0, utils_1.getValueFromWindow)('testKey');
expect(logger_1.logger.error).toHaveBeenCalledWith(errors_1.ERROR_MESSAGE.SSR_ENVIRONMENT_ERROR);
expect(result).toBeUndefined();
});
test('resetValueFromWindow should log error in SSR environment', () => {
delete global.window;
const result = (0, utils_1.resetValueFromWindow)('testKey');
expect(logger_1.logger.error).toHaveBeenCalledWith(errors_1.ERROR_MESSAGE.SSR_ENVIRONMENT_ERROR);
expect(result).toBe(false);
});
});
describe('resetValueFromWindow', () => {
beforeEach(() => {
window._tsEmbedSDK = {};
});
test('should reset existing key and return true', () => {
(0, utils_1.storeValueInWindow)('keyToReset', 'someValue');
expect((0, utils_1.getValueFromWindow)('keyToReset')).toBe('someValue');
const result = (0, utils_1.resetValueFromWindow)('keyToReset');
expect(result).toBe(true);
expect((0, utils_1.getValueFromWindow)('keyToReset')).toBe(undefined);
});
test('should return false when key does not exist', () => {
const result = (0, utils_1.resetValueFromWindow)('nonExistentKey');
expect(result).toBe(false);
});
test('should only reset the specified key', () => {
(0, utils_1.storeValueInWindow)('key1', 'value1');
(0, utils_1.storeValueInWindow)('key2', 'value2');
(0, utils_1.resetValueFromWindow)('key1');
expect((0, utils_1.getValueFromWindow)('key1')).toBe(undefined);
expect((0, utils_1.getValueFromWindow)('key2')).toBe('value2');
});
});
describe('validateHttpUrl', () => {
test.each([
['http URL', 'http://example.com'],
['https URL', 'https://example.com'],
['https URL with path', 'https://docs.example.com/spotter'],
['https URL with query params', 'https://example.com/path?foo=bar'],
])('should return [true, null] for valid %s', (_, url) => {
const [isValid, error] = (0, utils_1.validateHttpUrl)(url);
expect(isValid).toBe(true);
expect(error).toBeNull();
});
test.each([
['ftp protocol', 'ftp://example.com', 'ftp:'],
['file protocol', 'file:///path/to/file', 'file:'],
['javascript protocol', 'javascript:alert(1)', 'javascript:'],
])('should return [false, Error] for %s', (_, url, protocol) => {
const [isValid, error] = (0, utils_1.validateHttpUrl)(url);
expect(isValid).toBe(false);
expect(error).toBeInstanceOf(Error);
expect(error === null || error === void 0 ? void 0 : error.message).toContain('Invalid protocol');
expect(error === null || error === void 0 ? void 0 : error.message).toContain(protocol);
});
test.each([
['invalid URL format', 'not-a-valid-url'],
['empty string', ''],
['URL without protocol', 'example.com'],
])('should return [false, Error] for %s', (_, url) => {
const [isValid, error] = (0, utils_1.validateHttpUrl)(url);
expect(isValid).toBe(false);
expect(error).toBeInstanceOf(Error);
});
});
describe('setParamIfDefined', () => {
test.each([
['string value', 'testParam', 'testValue', false, 'testValue'],
['number value', 'numParam', 42, false, 42],
['truthy value as boolean', 'boolParam', 'truthy', true, true],
['falsy value as boolean', 'boolParam', 0, true, false],
])('should set %s correctly', (_, param, value, asBoolean, expected) => {
const queryParams = {};
(0, utils_1.setParamIfDefined)(queryParams, param, value, asBoolean);
expect(queryParams[param]).toBe(expected);
});
test('should not set param when value is undefined', () => {
const queryParams = {};
(0, utils_1.setParamIfDefined)(queryParams, 'testParam', undefined);
expect(queryParams.testParam).toBeUndefined();
});
});
});
// ---------------------------------------------------------------------------
// getSSOMarker
// ---------------------------------------------------------------------------
describe('getSSOMarker', () => {
test('returns tsSSOMarker query string with encoded markerId', () => {
expect((0, utils_1.getSSOMarker)('abc123')).toBe('tsSSOMarker=abc123');
});
test('URL-encodes special characters in markerId', () => {
expect((0, utils_1.getSSOMarker)('hello world')).toBe('tsSSOMarker=hello%20world');
expect((0, utils_1.getSSOMarker)('foo=bar&baz')).toBe('tsSSOMarker=foo%3Dbar%26baz');
});
test('handles empty string markerId', () => {
expect((0, utils_1.getSSOMarker)('')).toBe('tsSSOMarker=');
});
});
// ---------------------------------------------------------------------------
// deepMerge
// ---------------------------------------------------------------------------
describe('deepMerge', () => {
test('merges two flat objects', () => {
const result = (0, utils_1.deepMerge)({ a: 1 }, { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});
test('source overrides target for same keys', () => {
const result = (0, utils_1.deepMerge)({ a: 1, b: 2 }, { b: 99 });
expect(result).toEqual({ a: 1, b: 99 });
});
test('deep merges nested objects', () => {
const result = (0, utils_1.deepMerge)({ a: { x: 1, y: 2 } }, { a: { y: 99, z: 3 } });
expect(result).toEqual({ a: { x: 1, y: 99, z: 3 } });
});
test('handles empty source', () => {
const result = (0, utils_1.deepMerge)({ a: 1 }, {});
expect(result).toEqual({ a: 1 });
});
test('handles empty target', () => {
const result = (0, utils_1.deepMerge)({}, { b: 2 });
expect(result).toEqual({ b: 2 });
});
});
// ---------------------------------------------------------------------------
// getHostEventsConfig
// ---------------------------------------------------------------------------
describe('getHostEventsConfig', () => {
test('returns shouldBypassPayloadValidation and useHostEventsV2 from viewConfig', () => {
const viewConfig = { shouldBypassPayloadValidation: true, useHostEventsV2: false };
expect((0, utils_1.getHostEventsConfig)(viewConfig)).toEqual({
shouldBypassPayloadValidation: true,
useHostEventsV2: false,
});
});
test('returns undefined for missing keys', () => {
const result = (0, utils_1.getHostEventsConfig)({});
expect(result.shouldBypassPayloadValidation).toBeUndefined();
expect(result.useHostEventsV2).toBeUndefined();
});
test('ignores unrelated config properties', () => {
const viewConfig = {
shouldBypassPayloadValidation: false,
useHostEventsV2: true,
liveboardId: 'lb-1',
};
expect((0, utils_1.getHostEventsConfig)(viewConfig)).toEqual({
shouldBypassPayloadValidation: false,
useHostEventsV2: true,
});
});
});
// ---------------------------------------------------------------------------
// isWindowUndefined
// ---------------------------------------------------------------------------
describe('isWindowUndefined', () => {
test('returns false when window is defined (browser/jsdom env)', () => {
// In jest/jsdom, window is always defined
expect((0, utils_1.isWindowUndefined)()).toBe(false);
});
});
// ---------------------------------------------------------------------------
// getOffsetTop
// ---------------------------------------------------------------------------
describe('getOffsetTop', () => {
test('returns rect.top + window.scrollY', () => {
const mockElement = {
getBoundingClientRect: () => ({ top: 50 }),
};
Object.defineProperty(window, 'scrollY', { value: 100, configurable: true });
expect((0, utils_1.getOffsetTop)(mockElement)).toBe(150);
});
test('returns rect.top when scrollY is 0', () => {
const mockElement = {
getBoundingClientRect: () => ({ top: 30 }),
};
Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
expect((0, utils_1.getOffsetTop)(mockElement)).toBe(30);
});
});
// ---------------------------------------------------------------------------
// getDOMNode
// ---------------------------------------------------------------------------
describe('getDOMNode', () => {
test('returns the element from document.querySelector when passed a string selector', () => {
const div = document.createElement('div');
div.id = 'test-dom-node';
document.body.appendChild(div);
expect((0, utils_1.getDOMNode)('#test-dom-node')).toBe(div);
document.body.removeChild(div);
});
test('returns the element directly when passed an HTMLElement', () => {
const div = document.createElement('div');
expect((0, utils_1.getDOMNode)(div)).toBe(div);
});
test('returns null for a selector that matches nothing', () => {
expect((0, utils_1.getDOMNode)('#nonexistent-element-xyz')).toBeNull();
});
});
// ---------------------------------------------------------------------------
// getOperationNameFromQuery
// ---------------------------------------------------------------------------
describe('getOperationNameFromQuery', () => {
test('extracts operation name from a query string', () => {
expect((0, utils_1.getOperationNameFromQuery)('query GetUser { user { id } }')).toBe('GetUser');
});
test('extracts operation name from a mutation string', () => {
expect((0, utils_1.getOperationNameFromQuery)('mutation CreateUser { createUser { id } }')).toBe('CreateUser');
});
test('returns undefined when no operation name is present', () => {
expect((0, utils_1.getOperationNameFromQuery)('{ user { id } }')).toBeUndefined();
});
test('handles multiline query strings', () => {
const query = `
query FetchData {
data { id }
}
`;
expect((0, utils_1.getOperationNameFromQuery)(query)).toBe('FetchData');
});
});
//# sourceMappingURL=utils.spec.js.map