UNPKG

@thoughtspot/visual-embed-sdk

Version:
574 lines 33.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const auto_frame_renderer_1 = require("./auto-frame-renderer"); const types_1 = require("../types"); const index_1 = require("../index"); const authInstance = tslib_1.__importStar(require("../auth")); const ts_embed_1 = require("./ts-embed"); const liveboard_1 = require("./liveboard"); const test_utils_1 = require("../test/test-utils"); const thoughtSpotHost = 'tshost'; const TSMCP_SRC = `https://${thoughtSpotHost}/v2/?${types_1.Param.Tsmcp}=true#/embed/viz/lb1`; describe('startAutoMCPFrameRenderer', () => { let renderIFrameSpy; let getEmbedBasePathSpy; beforeAll(() => { (0, index_1.init)({ thoughtSpotHost, authType: types_1.AuthType.None, }); jest.spyOn(authInstance, 'postLoginService').mockImplementation(() => Promise.resolve(undefined)); jest.spyOn(window, 'alert').mockImplementation(() => undefined); }); beforeEach(() => { document.body.innerHTML = (0, test_utils_1.getDocumentBody)(); getEmbedBasePathSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'getEmbedBasePath').mockImplementation(function (query) { return `http://${thoughtSpotHost}/${query}#`; }); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame').mockResolvedValue(undefined); }); afterEach(() => { renderIFrameSpy.mockRestore(); getEmbedBasePathSpy.mockRestore(); }); // ─── helpers ────────────────────────────────────────────────────────────── /** Capture the src passed to renderIFrame for the first tsmcp iframe added */ async function captureRenderedSrc(viewConfig = {}) { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(viewConfig); const iframe = document.createElement('iframe'); iframe.src = TSMCP_SRC; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); observer.disconnect(); return capturedSrc; } /** * Trigger a full APP_INIT cycle via the actual TsEmbed message infrastructure. * Renders the replacement iframe into DOM so subscribeToMessageEvents fires, * then fires APP_INIT and returns the port.postMessage response payload. */ async function getAppInitResponse(viewConfig = {}) { var _a; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { const iframe = document.createElement('iframe'); iframe.src = src; this.setIframeElement(iframe); this.handleInsertionIntoDOM(iframe); this.subscribeToMessageEvents(); }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(viewConfig); const tsmcpIframe = document.createElement('iframe'); tsmcpIframe.src = TSMCP_SRC; document.body.appendChild(tsmcpIframe); await new Promise((r) => setTimeout(r, 50)); const embeddedIframe = document.querySelector('iframe'); const mockPort = { postMessage: jest.fn() }; (0, test_utils_1.postMessageToParent)(embeddedIframe.contentWindow, { type: types_1.EmbedEvent.APP_INIT, data: {} }, mockPort); await new Promise((r) => setTimeout(r, 50)); observer.disconnect(); return (_a = mockPort.postMessage.mock.calls[0]) === null || _a === void 0 ? void 0 : _a[0]; } // ─── MutationObserver setup ─────────────────────────────────────────────── describe('MutationObserver setup', () => { test('should return a MutationObserver', () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); expect(observer).toBeInstanceOf(MutationObserver); observer.disconnect(); }); test('should observe document.body with childList and subtree', () => { const observeSpy = jest.spyOn(MutationObserver.prototype, 'observe'); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); expect(observeSpy).toHaveBeenCalledWith(document.body, { childList: true, subtree: true, }); observer.disconnect(); observeSpy.mockRestore(); }); }); // ─── iframe detection via tsmcp param ───────────────────────────────────── describe('iframe detection via tsmcp param', () => { test('should process directly-added iframes with tsmcp=true', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/v2/?${types_1.Param.Tsmcp}=true#/embed/viz/lb1/tab1`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).toHaveBeenCalled(); observer.disconnect(); }); test('should not process iframes without tsmcp param', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/v2/?embedApp=true#/embed/viz/lb1`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).not.toHaveBeenCalled(); observer.disconnect(); }); test('should process tsmcp iframes nested inside added elements', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const wrapper = document.createElement('div'); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/?${types_1.Param.Tsmcp}=true`; wrapper.appendChild(iframe); document.body.appendChild(wrapper); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).toHaveBeenCalled(); observer.disconnect(); }); test('should not process nested iframes without tsmcp param', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const wrapper = document.createElement('div'); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/?embedApp=true`; wrapper.appendChild(iframe); document.body.appendChild(wrapper); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).not.toHaveBeenCalled(); observer.disconnect(); }); test('should ignore non-iframe element nodes', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const div = document.createElement('div'); div.textContent = `${types_1.Param.Tsmcp}=true`; document.body.appendChild(div); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).not.toHaveBeenCalled(); observer.disconnect(); }); test('should ignore text nodes', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const text = document.createTextNode('tsmcp=true'); document.body.appendChild(text); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).not.toHaveBeenCalled(); observer.disconnect(); }); test('should process multiple tsmcp iframes in one mutation', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const wrapper = document.createElement('div'); const iframe1 = document.createElement('iframe'); iframe1.src = `https://${thoughtSpotHost}/?${types_1.Param.Tsmcp}=true&id=1`; const iframe2 = document.createElement('iframe'); iframe2.src = `https://${thoughtSpotHost}/?${types_1.Param.Tsmcp}=true&id=2`; wrapper.appendChild(iframe1); wrapper.appendChild(iframe2); document.body.appendChild(wrapper); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).toHaveBeenCalledTimes(2); observer.disconnect(); }); test('should ignore iframes with invalid src URLs', async () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = 'about:blank'; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(renderIFrameSpy).not.toHaveBeenCalled(); observer.disconnect(); }); }); // ─── handleInsertionIntoDOM override ────────────────────────────────────── describe('handleInsertionIntoDOM override', () => { test('should replace the original iframe when renderIFrame inserts DOM', async () => { renderIFrameSpy.mockRestore(); const replaceSpy = jest.fn(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function () { const newIframe = document.createElement('iframe'); newIframe.src = 'https://replaced.example.com'; this.frameToReplace.replaceWith = replaceSpy; this.handleInsertionIntoDOM(newIframe); }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/?${types_1.Param.Tsmcp}=true`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(replaceSpy).toHaveBeenCalled(); observer.disconnect(); }); }); // ─── URL params forwarding ──────────────────────────────────────────────── describe('URL params forwarding', () => { test('should accept empty viewConfig', () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)({}); expect(observer).toBeInstanceOf(MutationObserver); observer.disconnect(); }); test('should accept no arguments', () => { const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); expect(observer).toBeInstanceOf(MutationObserver); observer.disconnect(); }); test('disabledActions → disableAction in rendered src', async () => { const src = await captureRenderedSrc({ disabledActions: [types_1.Action.Pin] }); expect(src).toContain('disableAction'); }); test('disabledActionReason → disableHint in rendered src', async () => { const src = await captureRenderedSrc({ disabledActions: [types_1.Action.Pin], disabledActionReason: 'Upgrade required', }); expect(src).toContain('disableHint'); }); test('hiddenActions → hideAction in rendered src as JSON array', async () => { const src = await captureRenderedSrc({ hiddenActions: [types_1.Action.Pin] }); expect(src).toContain(`hideAction=${JSON.stringify([types_1.Action.ReportError, types_1.Action.Pin])}`); }); test('visibleActions → visibleAction in rendered src', async () => { const src = await captureRenderedSrc({ visibleActions: [types_1.Action.Download] }); expect(src).toContain('visibleAction'); }); test('locale → locale param in rendered src', async () => { const src = await captureRenderedSrc({ locale: 'fr-FR' }); expect(src).toContain('locale=fr-FR'); }); test('showAlerts → showAlerts param in rendered src', async () => { const src = await captureRenderedSrc({ showAlerts: true }); expect(src).toContain('showAlerts=true'); }); test('exposeTranslationIDs → exposeTranslationIDs in rendered src', async () => { const src = await captureRenderedSrc({ exposeTranslationIDs: true }); expect(src).toContain('exposeTranslationIDs=true'); }); test('disableRedirectionLinksInNewTab → param in rendered src', async () => { const src = await captureRenderedSrc({ disableRedirectionLinksInNewTab: true }); expect(src).toContain('disableRedirectionLinksInNewTab=true'); }); test('overrideOrgId → orgId param in rendered src', async () => { const src = await captureRenderedSrc({ overrideOrgId: 42 }); expect(src).toContain('orgId=42'); }); test('linkOverride (V1) auto-upgrades to V2 in rendered src', async () => { const src = await captureRenderedSrc({ linkOverride: true }); expect(src).toContain('linkOverride=true'); expect(src).toContain('enableLinkOverridesV2=true'); }); test('enableLinkOverridesV2 → enableLinkOverridesV2 + linkOverride in rendered src', async () => { const src = await captureRenderedSrc({ enableLinkOverridesV2: true }); expect(src).toContain('enableLinkOverridesV2=true'); expect(src).toContain('linkOverride=true'); }); test('disableRedirectionLinksInNewTab auto-disables V2 link overrides', async () => { const src = await captureRenderedSrc({ enableLinkOverridesV2: true, disableRedirectionLinksInNewTab: true, }); expect(src).not.toContain('enableLinkOverridesV2=true'); expect(src).not.toContain('linkOverride=true'); expect(src).toContain('disableRedirectionLinksInNewTab=true'); }); test('disableRedirectionLinksInNewTab auto-disables V1 link override', async () => { const src = await captureRenderedSrc({ linkOverride: true, disableRedirectionLinksInNewTab: true, }); expect(src).not.toContain('linkOverride=true'); expect(src).toContain('disableRedirectionLinksInNewTab=true'); }); test('additionalFlags → merged into rendered src', async () => { const src = await captureRenderedSrc({ additionalFlags: { myFlag: 'hello', anotherFlag: 1 } }); expect(src).toContain('myFlag=hello'); expect(src).toContain('anotherFlag=1'); }); test('additionalFlags from viewConfig override those from init', async () => { const src = await captureRenderedSrc({ additionalFlags: { overrideFlag: 'view' } }); expect(src).toContain('overrideFlag=view'); }); test('insertInToSlide → insertInToSlide param in rendered src', async () => { const src = await captureRenderedSrc({ insertInToSlide: true }); expect(src).toContain('insertInToSlide=true'); }); test('customizations.iconSpriteUrl → iconSprite param in rendered src', async () => { const src = await captureRenderedSrc({ customizations: { iconSpriteUrl: 'https://cdn.example.com/icons.svg' }, }); expect(src).toContain('iconSprite=cdn.example.com/icons.svg'); }); test('customizations.content.stringIDsUrl → overrideStringIDsUrl param in rendered src', async () => { const src = await captureRenderedSrc({ customizations: { content: { stringIDsUrl: 'https://cdn.example.com/strings.json' } }, }); expect(src).toContain('overrideStringIDsUrl='); }); test('multiple disabledActions → all actions serialised in rendered src', async () => { const src = await captureRenderedSrc({ disabledActions: [types_1.Action.Pin, types_1.Action.Download, types_1.Action.Save], }); expect(src).toContain('disableAction'); expect(src).toContain(types_1.Action.Pin); expect(src).toContain(types_1.Action.Download); expect(src).toContain(types_1.Action.Save); }); test('multiple hiddenActions → all actions serialised in rendered src', async () => { var _a, _b; const src = await captureRenderedSrc({ hiddenActions: [types_1.Action.Pin, types_1.Action.Download], }); const hideParam = decodeURIComponent((_b = (_a = src.split('hideAction=')[1]) === null || _a === void 0 ? void 0 : _a.split('&')[0]) !== null && _b !== void 0 ? _b : ''); const parsed = JSON.parse(hideParam); expect(parsed).toContain(types_1.Action.Pin); expect(parsed).toContain(types_1.Action.Download); }); test('multiple visibleActions → all actions serialised in rendered src', async () => { var _a, _b; const src = await captureRenderedSrc({ visibleActions: [types_1.Action.Download, types_1.Action.Save, types_1.Action.Pin], }); const visibleParam = decodeURIComponent((_b = (_a = src.split('visibleAction=')[1]) === null || _a === void 0 ? void 0 : _a.split('&')[0]) !== null && _b !== void 0 ? _b : ''); const parsed = JSON.parse(visibleParam); expect(parsed).toContain(types_1.Action.Download); expect(parsed).toContain(types_1.Action.Save); expect(parsed).toContain(types_1.Action.Pin); }); test('rendered src always contains a query string before the hash', async () => { const src = await captureRenderedSrc(); expect(src).toMatch(/\?[^#]+#/); }); }); // ─── frameParams forwarding ─────────────────────────────────────────────── describe('frameParams forwarding', () => { test('frameParams.height and .width applied to the replacement iframe element', async () => { var _a, _b, _c; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockResolvedValue(undefined); const createIframeElSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'createIframeEl'); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)({ frameParams: { height: '600px', width: '100%' } }); const iframe = document.createElement('iframe'); iframe.src = TSMCP_SRC; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); if (createIframeElSpy.mock.calls.length > 0) { const created = (_a = createIframeElSpy.mock.results[0]) === null || _a === void 0 ? void 0 : _a.value; expect((_b = created === null || created === void 0 ? void 0 : created.style) === null || _b === void 0 ? void 0 : _b.height).toBe('600px'); expect((_c = created === null || created === void 0 ? void 0 : created.style) === null || _c === void 0 ? void 0 : _c.width).toBe('100%'); } createIframeElSpy.mockRestore(); observer.disconnect(); }); test('frameParams custom HTML attributes applied to the iframe element', async () => { var _a; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockResolvedValue(undefined); const createIframeElSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'createIframeEl'); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)({ frameParams: { height: '400px', width: '800px', 'data-testid': 'my-embed' }, }); const iframe = document.createElement('iframe'); iframe.src = TSMCP_SRC; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); if (createIframeElSpy.mock.calls.length > 0) { const created = (_a = createIframeElSpy.mock.results[0]) === null || _a === void 0 ? void 0 : _a.value; expect(created === null || created === void 0 ? void 0 : created.getAttribute('data-testid')).toBe('my-embed'); } createIframeElSpy.mockRestore(); observer.disconnect(); }); }); // ─── APP_INIT postMessage params forwarding ─────────────────────────────── // // These params are not in the iframe src URL — they travel via the APP_INIT // postMessage channel. AutoFrameRenderer inherits the TsEmbed APP_INIT // handler, so the full round-trip is tested here. describe('APP_INIT params forwarding', () => { test('customizations.style.customCSS included in APP_INIT response', async () => { var _a, _b, _c; const customizations = { style: { customCSS: { variables: { '--ts-var-root-background': '#fff' } } }, }; const response = await getAppInitResponse({ customizations }); expect((_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.customisations) === null || _b === void 0 ? void 0 : _b.style) === null || _c === void 0 ? void 0 : _c.customCSS).toEqual(customizations.style.customCSS); }); test('customizations.content.strings included in APP_INIT response', async () => { var _a, _b, _c, _d; const customizations = { content: { strings: { DATA: 'Data' } }, }; const response = await getAppInitResponse({ customizations }); expect((_d = (_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.customisations) === null || _b === void 0 ? void 0 : _b.content) === null || _c === void 0 ? void 0 : _c.strings) === null || _d === void 0 ? void 0 : _d.DATA).toBe('Data'); }); test('customActions included in APP_INIT response', async () => { var _a; const customActions = [ { id: 'my-action', name: 'My Action', position: 'PRIMARY', target: 'ANSWER', }, ]; const response = await getAppInitResponse({ customActions }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.customActions).toEqual(expect.arrayContaining([expect.objectContaining({ id: 'my-action' })])); }); test('shouldBypassPayloadValidation forwarded via APP_INIT', async () => { var _a; const response = await getAppInitResponse({ shouldBypassPayloadValidation: true }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.shouldBypassPayloadValidation).toBe(true); }); test('useHostEventsV2 forwarded via APP_INIT', async () => { var _a; const response = await getAppInitResponse({ useHostEventsV2: true }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.useHostEventsV2).toBe(true); }); test('refreshAuthTokenOnNearExpiry forwarded as embedExpiryInAuthToken', async () => { var _a; const response = await getAppInitResponse({ refreshAuthTokenOnNearExpiry: true }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.embedExpiryInAuthToken).toBe(true); }); test('interceptUrls forwarded via APP_INIT', async () => { var _a, _b; // The SDK expands InterceptedApiType enum values into resolved prism endpoint URLs. // Assert that the interceptUrls array is non-empty (i.e. the config was forwarded // and processed) rather than checking the resolved strings directly. const response = await getAppInitResponse({ interceptUrls: [types_1.InterceptedApiType.AnswerData], }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.interceptUrls).toBeInstanceOf(Array); expect((_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.interceptUrls.length).toBeGreaterThan(0); }); test('interceptTimeout forwarded via APP_INIT', async () => { var _a; const response = await getAppInitResponse({ interceptTimeout: 5000 }); expect((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.interceptTimeout).toBe(5000); }); }); // ─── getMCPIframeSrc URL construction ───────────────────────────────────── describe('getMCPIframeSrc URL construction', () => { test('should strip tsmcp param and merge embed params into rendered src', async () => { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/v2/?${types_1.Param.Tsmcp}=true&customParam=hello#/embed/viz`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(capturedSrc).not.toContain(`${types_1.Param.Tsmcp}=true`); expect(capturedSrc).toContain('customParam=hello'); expect(capturedSrc).toMatch(/\?[^#]+#/); observer.disconnect(); }); test('should preserve hash from original iframe src', async () => { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); iframe.src = `https://${thoughtSpotHost}/v2/?${types_1.Param.Tsmcp}=true#/embed/viz/lb123`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(capturedSrc).toContain('/embed/viz/lb123'); observer.disconnect(); }); test('should produce empty query string when no embed params and no source params', async () => { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)(); const iframe = document.createElement('iframe'); // Only tsmcp (stripped) — no other params iframe.src = `https://${thoughtSpotHost}/?${types_1.Param.Tsmcp}=true`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); // At minimum the base embed params (hostAppUrl, sdkVersion, etc.) are always present expect(capturedSrc).toMatch(/\?[^#]+#/); observer.disconnect(); }); test('source params take precedence over viewConfig params for the same key', async () => { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const observer = (0, auto_frame_renderer_1.startAutoMCPFrameRenderer)({ additionalFlags: { locale: 'en-US' } }); const iframe = document.createElement('iframe'); // Source overrides with de-DE iframe.src = `https://${thoughtSpotHost}/v2/?${types_1.Param.Tsmcp}=true&locale=de-DE#/embed/viz`; document.body.appendChild(iframe); await new Promise((r) => setTimeout(r, 50)); expect(capturedSrc).toContain('locale=de-DE'); observer.disconnect(); }); }); // ─── URL serialization parity with normal embeds ────────────────────────── // // These tests verify that array-typed params (hideAction, disableAction, // visibleAction) are serialized identically by startAutoMCPFrameRenderer // and by a standard LiveboardEmbed. Both must emit JSON arrays // (e.g. ["pin"]) not CSV (e.g. pin) so ThoughtSpot's app honours them. describe('URL serialization parity with LiveboardEmbed', () => { async function captureLiveboardSrc(viewConfig) { let capturedSrc = ''; renderIFrameSpy.mockRestore(); renderIFrameSpy = jest.spyOn(ts_embed_1.TsEmbed.prototype, 'renderIFrame') .mockImplementation(async function (src) { capturedSrc = src; }); const embed = new liveboard_1.LiveboardEmbed((0, test_utils_1.getRootEl)(), { liveboardId: 'test-lb', ...viewConfig, }); embed.render(); await new Promise((r) => setTimeout(r, 50)); return capturedSrc; } function getQueryParam(url, param) { const qIdx = url.indexOf('?'); const hIdx = url.indexOf('#'); if (qIdx === -1) return null; const qs = hIdx === -1 ? url.slice(qIdx + 1) : url.slice(qIdx + 1, hIdx); return new URLSearchParams(qs).get(param); } test.each([ ['hiddenActions', { hiddenActions: [types_1.Action.Pin] }, 'hideAction'], ['disabledActions', { disabledActions: [types_1.Action.Pin] }, 'disableAction'], ['visibleActions', { visibleActions: [types_1.Action.Download] }, 'visibleAction'], ])('%s: auto-renderer and LiveboardEmbed produce identical param format', async (_, viewConfig, paramName) => { const liveboardSrc = await captureLiveboardSrc(viewConfig); const autoSrc = await captureRenderedSrc(viewConfig); const liveboardValue = getQueryParam(liveboardSrc, paramName); const autoValue = getQueryParam(autoSrc, paramName); expect(autoValue).not.toBeNull(); expect(liveboardValue).not.toBeNull(); expect(autoValue).toBe(liveboardValue); }); test('hideAction value is a JSON array, not CSV', async () => { const autoSrc = await captureRenderedSrc({ hiddenActions: [types_1.Action.Pin] }); const liveboardSrc = await captureLiveboardSrc({ hiddenActions: [types_1.Action.Pin] }); const autoValue = getQueryParam(autoSrc, 'hideAction'); const liveboardValue = getQueryParam(liveboardSrc, 'hideAction'); // Both must parse as a JSON array — not CSV like "reportError,pin" expect(() => JSON.parse(autoValue)).not.toThrow(); expect(() => JSON.parse(liveboardValue)).not.toThrow(); expect(Array.isArray(JSON.parse(autoValue))).toBe(true); expect(Array.isArray(JSON.parse(liveboardValue))).toBe(true); expect(JSON.parse(autoValue)).toEqual(JSON.parse(liveboardValue)); }); }); }); //# sourceMappingURL=auto-frame-renderer.spec.js.map