UNPKG

@thoughtspot/visual-embed-sdk

Version:
593 lines 31.5 kB
import React from 'react'; import '@testing-library/jest-dom'; import '@testing-library/jest-dom/extend-expect'; import { 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').mockReturnValue(true); jest.spyOn(sessionService, 'getSessionInfo').mockReturnValue({ userGUID: 'abcd', }); spyOn(window, 'alert'); }); describe('React Components', () => { describe('SearchEmbed', () => { it('Should Render the Iframe with props', async () => { const { container } = render(React.createElement(SearchEmbed, { hideDataSources: true, className: "embedClass" })); 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=false&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`); }); it('Should attach event listeners', async (done) => { const userGUID = 'absfdfgd'; const { container } = render(React.createElement(SearchEmbed, { onInit: (e) => { expect(e.data).toHaveProperty('timestamp'); }, onAuthInit: (e) => { expect(e.data.userGUID).toEqual(userGUID); done(); } })); 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 (React.createElement(LiveboardEmbed, { ref: embedRef, liveboardId: "abcd", onLiveboardRendered: onLiveboardRendered })); }; const { container } = render(React.createElement(TestComponent, null)); 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(React.createElement(LiveboardEmbed, { liveboardId: "abcd", runtimeFilters: [ { columnName: 'revenue', operator: RuntimeFilterOp.EQ, values: [100], }, ], excludeRuntimeFiltersfromURL: false })); 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(React.createElement(LiveboardEmbed, { liveboardId: "abcd", className: "def" })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('div')).not.toBe(null); expect(container.querySelector('div').classList.contains('def')).toBe(true); const { container: containerSibling } = render(React.createElement(LiveboardEmbed, { liveboardId: "abcd", className: "def", insertAsSibling: true })); 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(React.createElement(LiveboardEmbed, { liveboardId: "abcd", className: "def" })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('div')).not.toBe(null); expect(container.querySelector('div').classList.contains('def')).toBe(true); const { container: containerSibling } = render(React.createElement(LiveboardEmbed, { liveboardId: "abcd", className: "def", insertAsSibling: true })); 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(React.createElement(SearchBarEmbed, { className: "embedClass", dataSource: 'test', searchOptions: { searchTokenString: '[revenue]', executeSearch: true, } })); 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(React.createElement(SpotterMessage, { message: mockMessage })); 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(React.createElement(SpotterMessage, { message: mockMessage, query: "show me sales" })); 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(React.createElement(SpotterMessage, { message: mockMessage, className: "custom-class" })); 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(React.createElement(LiveboardEmbed, { liveboardId: "test" })); }).not.toThrow(); expect(() => { render(React.createElement(SearchEmbed, { dataSource: "test" })); }).not.toThrow(); expect(() => { render(React.createElement(AppEmbed, { showPrimaryNavbar: false })); }).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 () => { var _a; const { container } = render(React.createElement(LiveboardEmbed, { liveboardId: "test-liveboard", insertAsSibling: true })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('span')).not.toBe(null); expect((_a = container.querySelector('span')) === null || _a === void 0 ? void 0 : _a.style.position).toBe('absolute'); }); it('Should render SearchEmbed with insertAsSibling', async () => { var _a; const { container } = render(React.createElement(SearchEmbed, { dataSource: "test-datasource", insertAsSibling: true })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('span')).not.toBe(null); expect((_a = container.querySelector('span')) === null || _a === void 0 ? void 0 : _a.style.position).toBe('absolute'); }); it('Should render AppEmbed with insertAsSibling', async () => { var _a; const { container } = render(React.createElement(AppEmbed, { showPrimaryNavbar: false, insertAsSibling: true })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('span')).not.toBe(null); expect((_a = container.querySelector('span')) === null || _a === void 0 ? void 0 : _a.style.position).toBe('absolute'); }); it('Should render SearchBarEmbed with insertAsSibling', async () => { var _a; const { container } = render(React.createElement(SearchBarEmbed, { dataSource: "test-datasource", insertAsSibling: true })); await waitFor(() => getIFrameEl(container)); expect(container.querySelector('span')).not.toBe(null); expect((_a = container.querySelector('span')) === null || _a === void 0 ? void 0 : _a.style.position).toBe('absolute'); }); it('Should render components with both insertAsSibling and className', async () => { const { container } = render(React.createElement(LiveboardEmbed, { liveboardId: "test-liveboard", insertAsSibling: true, className: "custom-class" })); 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 React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); }); 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 React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); }); it('Should return error when service is not initialized', async () => { let sendMessageResult; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); // Call sendMessage immediately before service has time to initialize sendMessageResult = sendMessage('test query'); return React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); 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; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); sendMessageFunction = sendMessage; return React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); // 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; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); sendMessageFunction = sendMessage; return React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); // 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 }) => { const { sendMessage } = useSpotterAgent(config); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const config1 = { worksheetId: 'test1' }; const config2 = { worksheetId: 'test2' }; const { rerender } = render(React.createElement(TestComponent, { config: config1 })); // Should not throw when config changes expect(() => { rerender(React.createElement(TestComponent, { config: config2 })); }).not.toThrow(); }); it('Should handle unmounting without errors', () => { const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const { unmount } = render(React.createElement(TestComponent, null)); // Should not throw when unmounting expect(() => { unmount(); }).not.toThrow(); }); it('Should create stable hook structure', () => { let hookResult1, hookResult2; const TestComponent = ({ counter }) => { const result = useSpotterAgent({ worksheetId: 'test-worksheet' }); if (counter === 1) { hookResult1 = result; } else { hookResult2 = result; } return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { counter: 1 })); rerender(React.createElement(TestComponent, { counter: 2 })); // 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 }) => { const { sendMessage } = useSpotterAgent({ worksheetId }); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { worksheetId: "worksheet1" })); // Should handle different worksheet IDs expect(() => { rerender(React.createElement(TestComponent, { worksheetId: "worksheet2" })); rerender(React.createElement(TestComponent, { worksheetId: "worksheet3" })); }).not.toThrow(); }); it('Should handle empty query strings', () => { let sendMessageFunction; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); sendMessageFunction = sendMessage; return React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); // 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 React.createElement("div", null, "Test"); }; // Should not throw with complex config expect(() => { render(React.createElement(TestComponent, null)); }).not.toThrow(); }); it('Should maintain function identity across re-renders with same config', () => { let sendMessage1, sendMessage2; const TestComponent = ({ forceRender }) => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); if (forceRender === 1) { sendMessage1 = sendMessage; } else { sendMessage2 = sendMessage; } return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { forceRender: 1 })); rerender(React.createElement(TestComponent, { forceRender: 2 })); // 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; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); capturedSendMessage = sendMessage; return React.createElement("div", null, "Test"); }; const { unmount } = render(React.createElement(TestComponent, null)); // 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 }) => { const { sendMessage } = useSpotterAgent({ worksheetId }); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { worksheetId: "worksheet1" })); // This should trigger the cleanup and create new service rerender(React.createElement(TestComponent, { worksheetId: "worksheet2" })); // Should still work after rerender expect(() => { rerender(React.createElement(TestComponent, { worksheetId: "worksheet3" })); }).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 React.createElement("div", null, "Test ", index); }; expect(() => { const { unmount } = render(React.createElement(TestComponent, null)); unmount(); }).not.toThrow(); }); }); it('Should handle rapid config changes', () => { const TestComponent = ({ worksheetId }) => { const { sendMessage } = useSpotterAgent({ worksheetId }); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { worksheetId: "worksheet1" })); // Rapid config changes to test cleanup logic for (let i = 2; i <= 10; i++) { rerender(React.createElement(TestComponent, { worksheetId: `worksheet${i}` })); } // Should still work after many changes expect(() => { rerender(React.createElement(TestComponent, { worksheetId: "final-worksheet" })); }).not.toThrow(); }); it('Should handle sendMessage with different query types', () => { let sendMessageFunction; const TestComponent = () => { const { sendMessage } = useSpotterAgent({ worksheetId: 'test-worksheet' }); sendMessageFunction = sendMessage; return React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); // 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 }) => { const { sendMessage } = useSpotterAgent({ worksheetId }); expect(sendMessage).toBeDefined(); return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { worksheetId: "worksheet1" })); // This should trigger the "if (serviceRef.current)" branch in useEffect rerender(React.createElement(TestComponent, { worksheetId: "worksheet1" })); rerender(React.createElement(TestComponent, { worksheetId: "worksheet2" })); rerender(React.createElement(TestComponent, { worksheetId: "worksheet3" })); // Multiple rapid changes should exercise the cleanup logic for (let i = 0; i < 5; i++) { rerender(React.createElement(TestComponent, { worksheetId: `worksheet${i}` })); } }); 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 React.createElement("div", null, "Test ", index); }; const { unmount } = render(React.createElement(TestComponent, null)); 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 = () => React.createElement("div", null, "Test"); render(React.createElement(TestComponent, null)); }).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 React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); }); it('Should handle useInit with different config changes', () => { const TestComponent = ({ host }) => { const authEE = useInit({ thoughtSpotHost: host, authType: AuthType.None }); expect(authEE).toBeDefined(); return React.createElement("div", null, "Test"); }; const { rerender } = render(React.createElement(TestComponent, { host: "localhost" })); // Change config to test useDeepCompareEffect rerender(React.createElement(TestComponent, { host: "localhost2" })); rerender(React.createElement(TestComponent, { host: "localhost3" })); }); 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 React.createElement("div", null, "Test"); }; render(React.createElement(TestComponent, null)); }); }); }); 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])) .toBe(true)); }); }); //# sourceMappingURL=index.spec.js.map