import React, { useEffect } from 'react';
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
import {
cleanup, fireEvent, render, waitFor,
} from '@testing-library/react';
import {
Action, EmbedEvent, HostEvent, RuntimeFilterOp,
} from '../types';
import {
executeAfterWait,
getIFrameEl,
getIFrameSrc,
postMessageToParent,
mockMessageChannel,
} from '../test/test-utils';
import {
SearchEmbed, AppEmbed, LiveboardEmbed, useEmbedRef, SearchBarEmbed, PreRenderedLiveboardEmbed, PreRenderedSearchEmbed, PreRenderedAppEmbed, useSpotterAgent, SpotterMessage, useInit
} from './index';
import * as allExports from './index';
import {
AuthType, init,
} from '../index';
import { version } from '../../package.json';
import * as auth from '../auth';
import * as sessionService from '../utils/sessionInfoService';
const thoughtSpotHost = 'localhost';
beforeAll(() => {
init({
thoughtSpotHost,
authType: AuthType.None,
});
jest.spyOn(auth, 'postLoginService').mockImplementation(() => Promise.resolve(undefined));
jest.spyOn(sessionService, 'getSessionInfo').mockReturnValue({
userGUID: 'abcd',
} as any);
jest.spyOn(window, 'alert');
});
describe('React Components', () => {
describe('SearchEmbed', () => {
it('Should Render the Iframe with props', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(
getIFrameEl(container).parentElement.classList.contains(
'embedClass',
),
).toBe(true);
expect(getIFrameSrc(container)).toBe(
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22,%22editACopy%22,%22saveAsView%22,%22updateTSL%22,%22editTSL%22,%22onDeleteAnswer%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&enableDataPanelV2=true&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`,
);
});
it('Should attach event listeners', async () => {
const userGUID = 'absfdfgd';
const { container } = render(
{
expect(e.data).toHaveProperty('timestamp');
}}
onAuthInit={(e) => {
expect(e.data.userGUID).toEqual(userGUID);
}}
/>,
);
await waitFor(() => getIFrameEl(container));
const iframe = getIFrameEl(container);
postMessageToParent(iframe.contentWindow, {
type: EmbedEvent.AuthInit,
data: {
userGUID,
},
});
});
});
describe('AppEmbed', () => {
//
});
describe('LiveboardEmbed', () => {
//
it('Should be able to trigger events on the embed using refs', async () => {
mockMessageChannel();
const TestComponent = () => {
const embedRef = useEmbedRef();
const onLiveboardRendered = () => {
embedRef.current.trigger(HostEvent.SetVisibleVizs, ['viz1', 'viz2']);
};
return (
);
};
const { container } = render();
await waitFor(() => getIFrameEl(container));
const iframe = getIFrameEl(container);
jest.spyOn(iframe.contentWindow, 'postMessage');
postMessageToParent(iframe.contentWindow, {
type: EmbedEvent.LiveboardRendered,
data: {
userGUID: 'abcd',
},
});
await executeAfterWait(() => {
expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
{
type: HostEvent.SetVisibleVizs,
data: ['viz1', 'viz2'],
},
`http://${thoughtSpotHost}`,
expect.anything(),
);
});
});
it('Should render liveboard with runtime filters', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(getIFrameSrc(container)).toContain('col1=revenue&op1=EQ&val1=100');
});
it('Should have the correct container element', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('div')).not.toBe(null);
expect(
container.querySelector('div').classList.contains('def'),
).toBe(true);
const { container: containerSibling } = render(
,
);
await waitFor(() => getIFrameEl(containerSibling));
expect(containerSibling.querySelector('span')).not.toBe(null);
expect(containerSibling.querySelector('span').style.position).toBe(
'absolute',
);
expect(
getIFrameEl(containerSibling).classList.contains('def'),
).toBe(true);
expect(containerSibling.querySelector('div')).toBe(null);
});
it('Should have the correct container element', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('div')).not.toBe(null);
expect(
container.querySelector('div').classList.contains('def'),
).toBe(true);
const { container: containerSibling } = render(
,
);
await waitFor(() => getIFrameEl(containerSibling));
expect(containerSibling.querySelector('span')).not.toBe(null);
expect(containerSibling.querySelector('span').style.position).toBe(
'absolute',
);
expect(
getIFrameEl(containerSibling).classList.contains('def'),
).toBe(true);
expect(containerSibling.querySelector('div')).toBe(null);
});
});
describe('SearchBarEmbed', () => {
it('Should Render the Iframe with props', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(
getIFrameEl(container).parentElement.classList.contains(
'embedClass',
),
).toBe(true);
expect(getIFrameSrc(container)).toBe(
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&dataSources=[%22test%22]&searchTokenString=%5Brevenue%5D&executeSearch=true&useLastSelectedSources=false&isSearchEmbed=true#/embed/search-bar-embed`,
);
});
});
describe('SpotterMessage', () => {
const mockMessage = {
sessionId: "session123",
genNo: 1,
acSessionId: "acSession123",
acGenNo: 2,
worksheetId: "worksheet123",
convId: "conv123",
messageId: "message123"
};
it('Should render the SpotterMessage component with required props', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(getIFrameEl(container)).not.toBe(null);
expect(getIFrameSrc(container)).toContain('sessionId=session123');
expect(getIFrameSrc(container)).toContain('genNo=1');
expect(getIFrameSrc(container)).toContain('acSessionId=acSession123');
expect(getIFrameSrc(container)).toContain('acGenNo=2');
});
it('Should render the SpotterMessage component with optional query', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(getIFrameEl(container)).not.toBe(null);
expect(getIFrameSrc(container)).toContain('sessionId=session123');
});
it('Should have the correct container element with className', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(
getIFrameEl(container).parentElement.classList.contains('custom-class'),
).toBe(true);
});
// Note: insertAsSibling is not supported for SpotterMessage as it's
// not part of the allowed props
});
describe('Component Factory Coverage', () => {
it('Should test basic component creation', () => {
expect(() => {
render();
}).not.toThrow();
expect(() => {
render();
}).not.toThrow();
expect(() => {
render();
}).not.toThrow();
});
it('Should test component factory existence', () => {
expect(PreRenderedLiveboardEmbed).toBeDefined();
expect(PreRenderedSearchEmbed).toBeDefined();
expect(PreRenderedAppEmbed).toBeDefined();
expect(typeof PreRenderedLiveboardEmbed).toBe('object');
expect(typeof PreRenderedSearchEmbed).toBe('object');
expect(typeof PreRenderedAppEmbed).toBe('object');
});
});
describe('Components with insertAsSibling', () => {
it('Should render LiveboardEmbed with insertAsSibling', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('span')).not.toBe(null);
expect(container.querySelector('span')?.style.position).toBe('absolute');
});
it('Should render SearchEmbed with insertAsSibling', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('span')).not.toBe(null);
expect(container.querySelector('span')?.style.position).toBe('absolute');
});
it('Should render AppEmbed with insertAsSibling', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('span')).not.toBe(null);
expect(container.querySelector('span')?.style.position).toBe('absolute');
});
it('Should render SearchBarEmbed with insertAsSibling', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('span')).not.toBe(null);
expect(container.querySelector('span')?.style.position).toBe('absolute');
});
it('Should render components with both insertAsSibling and className', async () => {
const { container } = render(
,
);
await waitFor(() => getIFrameEl(container));
expect(container.querySelector('span')).not.toBe(null);
expect(getIFrameEl(container).classList.contains('custom-class')).toBe(true);
});
});
describe('useSpotterAgent', () => {
it('Should return an object with sendMessage function', () => {
const TestComponent = () => {
const spotterAgent = useSpotterAgent({ worksheetId: 'test-worksheet' });
expect(typeof spotterAgent).toBe('object');
expect(typeof spotterAgent.sendMessage).toBe('function');
return Test
;
};
render();
});
it('Should have proper sendMessage callback structure', () => {
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
// Test that sendMessage is a function that accepts a string
expect(typeof sendMessage).toBe('function');
expect(sendMessage.length).toBe(1); // Should accept one parameter
return Test
;
};
render();
});
it('Should return error when service is not initialized', async () => {
let sendMessageResult: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
// Call sendMessage immediately before service has time to
// initialize
sendMessageResult = sendMessage('test query');
return Test
;
};
render();
const result = await sendMessageResult;
expect(result).toEqual({
error: expect.any(Error)
});
expect(result.error.message).toBe('SpotterAgent not initialized');
});
it('Should call sendMessage and handle async behavior', async () => {
let sendMessageFunction: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
sendMessageFunction = sendMessage;
return Test
;
};
render();
// Test that sendMessage is a function
expect(typeof sendMessageFunction).toBe('function');
// Call sendMessage - should not throw
expect(() => {
sendMessageFunction('test query');
}).not.toThrow();
});
it('Should handle multiple calls to sendMessage', async () => {
let sendMessageFunction: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
sendMessageFunction = sendMessage;
return Test
;
};
render();
// Multiple calls should not throw
expect(() => {
sendMessageFunction('query 1');
sendMessageFunction('query 2');
sendMessageFunction('query 3');
}).not.toThrow();
});
it('Should handle config object changes', () => {
const TestComponent = ({ config }: { config: any }) => {
const { sendMessage } = useSpotterAgent(config);
expect(sendMessage).toBeDefined();
return Test
;
};
const config1 = { worksheetId: 'test1' };
const config2 = { worksheetId: 'test2' };
const { rerender } = render();
// Should not throw when config changes
expect(() => {
rerender();
}).not.toThrow();
});
it('Should handle unmounting without errors', () => {
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
expect(sendMessage).toBeDefined();
return Test
;
};
const { unmount } = render();
// Should not throw when unmounting
expect(() => {
unmount();
}).not.toThrow();
});
it('Should create stable hook structure', () => {
let hookResult1: any, hookResult2: any;
const TestComponent = ({ counter }: { counter: number }) => {
const result = useSpotterAgent({ worksheetId: 'test-worksheet' });
if (counter === 1) {
hookResult1 = result;
} else {
hookResult2 = result;
}
return Test
;
};
const { rerender } = render();
rerender();
// Both should have same structure
expect(hookResult1).toEqual({ sendMessage: expect.any(Function) });
expect(hookResult2).toEqual({ sendMessage: expect.any(Function) });
});
it('Should handle different worksheet IDs', () => {
const TestComponent = ({ worksheetId }: { worksheetId: string }) => {
const { sendMessage } = useSpotterAgent({ worksheetId });
expect(sendMessage).toBeDefined();
return Test
;
};
const { rerender } = render();
// Should handle different worksheet IDs
expect(() => {
rerender();
rerender();
}).not.toThrow();
});
it('Should handle empty query strings', () => {
let sendMessageFunction: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
sendMessageFunction = sendMessage;
return Test
;
};
render();
// Should handle empty strings
expect(() => {
sendMessageFunction('');
sendMessageFunction(' ');
}).not.toThrow();
});
it('Should handle complex config objects', () => {
const complexConfig = {
worksheetId: 'test-worksheet',
hiddenActions: [Action.ReportError],
className: 'test-class',
searchOptions: {
searchQuery: 'test query'
}
};
const TestComponent = () => {
const { sendMessage } = useSpotterAgent(complexConfig);
expect(sendMessage).toBeDefined();
return Test
;
};
// Should not throw with complex config
expect(() => {
render();
}).not.toThrow();
});
it('Should maintain function identity across re-renders with same config', () => {
let sendMessage1: any, sendMessage2: any;
const TestComponent = ({ forceRender }: { forceRender: number }) => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
if (forceRender === 1) {
sendMessage1 = sendMessage;
} else {
sendMessage2 = sendMessage;
}
return Test
;
};
const { rerender } = render();
rerender();
// Functions should exist
expect(sendMessage1).toBeDefined();
expect(sendMessage2).toBeDefined();
expect(typeof sendMessage1).toBe('function');
expect(typeof sendMessage2).toBe('function');
});
it('Should handle sendMessage calls with null service ref', async () => {
let capturedSendMessage: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
capturedSendMessage = sendMessage;
return Test
;
};
const { unmount } = render();
// Unmount to trigger cleanup
unmount();
// Now call sendMessage after unmount - should return error
const result = await capturedSendMessage('test query');
expect(result).toEqual({
error: expect.any(Error)
});
});
it('Should test service ref cleanup on config change', () => {
const TestComponent = ({ worksheetId }: { worksheetId: string }) => {
const { sendMessage } = useSpotterAgent({ worksheetId });
expect(sendMessage).toBeDefined();
return Test
;
};
const { rerender } = render();
// This should trigger the cleanup and create new service
rerender();
// Should still work after rerender
expect(() => {
rerender();
}).not.toThrow();
});
it('Should test different config variations', () => {
const configs = [
{ worksheetId: 'test1' },
{ worksheetId: 'test2', hiddenActions: [Action.ReportError] },
{ worksheetId: 'test3', className: 'test-class' },
{ worksheetId: 'test4', searchOptions: { searchQuery: 'test' } }
];
configs.forEach((config, index) => {
const TestComponent = () => {
const { sendMessage } = useSpotterAgent(config);
expect(sendMessage).toBeDefined();
return Test {index}
;
};
expect(() => {
const { unmount } = render();
unmount();
}).not.toThrow();
});
});
it('Should handle rapid config changes', () => {
const TestComponent = ({ worksheetId }: { worksheetId: string }) => {
const { sendMessage } = useSpotterAgent({ worksheetId });
expect(sendMessage).toBeDefined();
return Test
;
};
const { rerender } = render();
// Rapid config changes to test cleanup logic
for (let i = 2; i <= 10; i++) {
rerender();
}
// Should still work after many changes
expect(() => {
rerender();
}).not.toThrow();
});
it('Should handle sendMessage with different query types', () => {
let sendMessageFunction: any;
const TestComponent = () => {
const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' });
sendMessageFunction = sendMessage;
return Test
;
};
render();
// Test different query types
const queries = [
'simple query',
'query with numbers 123',
'query with special chars !@#$%',
'very long query that might test different code paths in the system when processing',
'',
' whitespace ',
'null',
'undefined'
];
queries.forEach(query => {
expect(() => {
sendMessageFunction(query);
}).not.toThrow();
});
});
it('Should handle service ref cleanup when it already exists', () => {
const TestComponent = ({ worksheetId }: { worksheetId: string }) => {
const { sendMessage } = useSpotterAgent({ worksheetId });
expect(sendMessage).toBeDefined();
return Test
;
};
const { rerender } = render();
// This should trigger the "if (serviceRef.current)" branch in
// useEffect
rerender();
rerender();
rerender();
// Multiple rapid changes should exercise the cleanup logic
for (let i = 0; i < 5; i++) {
rerender();
}
});
it('Should test various config combinations to hit all branches', () => {
const testConfigs = [
{ worksheetId: 'test1' },
{ worksheetId: 'test2', className: 'custom-class' },
{ worksheetId: 'test3', hiddenActions: [Action.ReportError] },
{ worksheetId: 'test4', searchOptions: { searchQuery: 'test' } },
{ worksheetId: 'test5', insertAsSibling: true },
{ worksheetId: 'test6', insertAsSibling: false },
];
testConfigs.forEach((config, index) => {
const TestComponent = () => {
const { sendMessage } = useSpotterAgent(config);
expect(sendMessage).toBeDefined();
return Test {index}
;
};
const { unmount } = render();
unmount();
});
});
});
describe('Component Props and Functions', () => {
it('Should have PreRenderedLiveboardEmbed component', () => {
expect(PreRenderedLiveboardEmbed).toBeDefined();
expect(typeof PreRenderedLiveboardEmbed).toBe('object');
});
it('Should have useInit hook', () => {
expect(typeof useInit).toBe('function');
});
it('Should test basic component factory patterns', () => {
// Test that components can be created without errors
expect(() => {
const TestComponent = () => Test
;
render();
}).not.toThrow();
});
});
describe('Hook Coverage', () => {
it('Should have useInit function available', () => {
expect(typeof useInit).toBe('function');
});
it('Should test useInit hook basic functionality', () => {
const TestComponent = () => {
const authEE = useInit({
thoughtSpotHost: 'localhost',
authType: AuthType.None
});
expect(authEE).toBeDefined();
expect(authEE.current).toBeDefined();
return Test
;
};
render();
});
it('Should handle useInit with different config changes', () => {
const TestComponent = ({ host }: { host: string }) => {
const authEE = useInit({
thoughtSpotHost: host,
authType: AuthType.None
});
expect(authEE).toBeDefined();
return Test
;
};
const { rerender } = render();
// Change config to test useDeepCompareEffect
rerender();
rerender();
});
it('Should test useInit with complex config objects', () => {
const TestComponent = () => {
const authEE = useInit({
thoughtSpotHost: 'localhost',
authType: AuthType.None,
suppressNoCookieAccessAlert: true,
suppressErrorAlerts: true
});
expect(authEE).toBeDefined();
return Test
;
};
render();
});
});
});
describe('allExports', () => {
it('should have exports', () => {
expect(typeof allExports).toBe('object');
});
it('should not have undefined exports', () => {
Object.keys(allExports).forEach(
(exportKey) => expect(
Boolean(allExports[exportKey as keyof typeof allExports]),
)
.toBe(true),
);
});
});