@thoughtspot/visual-embed-sdk 1.50.0 → 1.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/css-variables.d.ts +95 -0
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +136 -15
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js +59 -37
  7. package/cjs/src/embed/app.js.map +1 -1
  8. package/cjs/src/embed/app.spec.js +419 -149
  9. package/cjs/src/embed/app.spec.js.map +1 -1
  10. package/cjs/src/embed/conversation.d.ts +230 -1
  11. package/cjs/src/embed/conversation.d.ts.map +1 -1
  12. package/cjs/src/embed/conversation.js +30 -16
  13. package/cjs/src/embed/conversation.js.map +1 -1
  14. package/cjs/src/embed/conversation.spec.js +221 -17
  15. package/cjs/src/embed/conversation.spec.js.map +1 -1
  16. package/cjs/src/embed/events.spec.js +635 -0
  17. package/cjs/src/embed/events.spec.js.map +1 -1
  18. package/cjs/src/embed/host-events.spec.js +151 -0
  19. package/cjs/src/embed/host-events.spec.js.map +1 -1
  20. package/cjs/src/embed/hostEventClient/contracts.d.ts +51 -14
  21. package/cjs/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  22. package/cjs/src/embed/hostEventClient/contracts.js +11 -1
  23. package/cjs/src/embed/hostEventClient/contracts.js.map +1 -1
  24. package/cjs/src/embed/hostEventClient/host-event-client.d.ts +1 -0
  25. package/cjs/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  26. package/cjs/src/embed/hostEventClient/host-event-client.js +9 -0
  27. package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
  28. package/cjs/src/embed/hostEventClient/host-event-client.spec.js +218 -0
  29. package/cjs/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  30. package/cjs/src/embed/hostEventClient/utils.d.ts +2 -0
  31. package/cjs/src/embed/hostEventClient/utils.d.ts.map +1 -1
  32. package/cjs/src/embed/hostEventClient/utils.js +32 -2
  33. package/cjs/src/embed/hostEventClient/utils.js.map +1 -1
  34. package/cjs/src/embed/hostEventClient/utils.spec.js +178 -0
  35. package/cjs/src/embed/hostEventClient/utils.spec.js.map +1 -1
  36. package/cjs/src/embed/liveboard.d.ts +42 -1
  37. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  38. package/cjs/src/embed/liveboard.js +26 -6
  39. package/cjs/src/embed/liveboard.js.map +1 -1
  40. package/cjs/src/embed/liveboard.spec.js +83 -2
  41. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  42. package/cjs/src/embed/spotter-utils.d.ts +8 -1
  43. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  44. package/cjs/src/embed/spotter-utils.js +14 -1
  45. package/cjs/src/embed/spotter-utils.js.map +1 -1
  46. package/cjs/src/embed/spotter-utils.spec.js +61 -0
  47. package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
  48. package/cjs/src/embed/spotter-viz-utils.d.ts +34 -0
  49. package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -1
  50. package/cjs/src/embed/spotter-viz-utils.js.map +1 -1
  51. package/cjs/src/embed/spotter-viz-utils.spec.js +17 -0
  52. package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -1
  53. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  54. package/cjs/src/embed/ts-embed.js +29 -7
  55. package/cjs/src/embed/ts-embed.js.map +1 -1
  56. package/cjs/src/embed/ts-embed.spec.js +146 -0
  57. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  58. package/cjs/src/errors.d.ts +1 -0
  59. package/cjs/src/errors.d.ts.map +1 -1
  60. package/cjs/src/errors.js +1 -0
  61. package/cjs/src/errors.js.map +1 -1
  62. package/cjs/src/index.d.ts +2 -2
  63. package/cjs/src/index.d.ts.map +1 -1
  64. package/cjs/src/index.js +3 -2
  65. package/cjs/src/index.js.map +1 -1
  66. package/cjs/src/mixpanel-service.d.ts +4 -0
  67. package/cjs/src/mixpanel-service.d.ts.map +1 -1
  68. package/cjs/src/mixpanel-service.js +4 -0
  69. package/cjs/src/mixpanel-service.js.map +1 -1
  70. package/cjs/src/react/util.spec.js +251 -0
  71. package/cjs/src/react/util.spec.js.map +1 -1
  72. package/cjs/src/types.d.ts +573 -11
  73. package/cjs/src/types.d.ts.map +1 -1
  74. package/cjs/src/types.js +519 -2
  75. package/cjs/src/types.js.map +1 -1
  76. package/cjs/src/utils/processData.spec.js +93 -0
  77. package/cjs/src/utils/processData.spec.js.map +1 -1
  78. package/cjs/src/utils/processTrigger.spec.js +61 -0
  79. package/cjs/src/utils/processTrigger.spec.js.map +1 -1
  80. package/dist/{index-B5xoN6Cp.js → index-aBzH5ZwF.js} +1 -1
  81. package/dist/src/css-variables.d.ts +95 -0
  82. package/dist/src/css-variables.d.ts.map +1 -1
  83. package/dist/src/embed/app.d.ts +136 -15
  84. package/dist/src/embed/app.d.ts.map +1 -1
  85. package/dist/src/embed/conversation.d.ts +230 -1
  86. package/dist/src/embed/conversation.d.ts.map +1 -1
  87. package/dist/src/embed/hostEventClient/contracts.d.ts +51 -14
  88. package/dist/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  89. package/dist/src/embed/hostEventClient/host-event-client.d.ts +1 -0
  90. package/dist/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  91. package/dist/src/embed/hostEventClient/utils.d.ts +2 -0
  92. package/dist/src/embed/hostEventClient/utils.d.ts.map +1 -1
  93. package/dist/src/embed/liveboard.d.ts +42 -1
  94. package/dist/src/embed/liveboard.d.ts.map +1 -1
  95. package/dist/src/embed/spotter-utils.d.ts +8 -1
  96. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  97. package/dist/src/embed/spotter-viz-utils.d.ts +34 -0
  98. package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -1
  99. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  100. package/dist/src/errors.d.ts +1 -0
  101. package/dist/src/errors.d.ts.map +1 -1
  102. package/dist/src/index.d.ts +2 -2
  103. package/dist/src/index.d.ts.map +1 -1
  104. package/dist/src/mixpanel-service.d.ts +4 -0
  105. package/dist/src/mixpanel-service.d.ts.map +1 -1
  106. package/dist/src/types.d.ts +573 -11
  107. package/dist/src/types.d.ts.map +1 -1
  108. package/dist/tsembed-react.es.js +936 -142
  109. package/dist/tsembed-react.js +935 -141
  110. package/dist/tsembed.es.js +935 -142
  111. package/dist/tsembed.js +933 -140
  112. package/dist/visual-embed-sdk-react-full.d.ts +1147 -44
  113. package/dist/visual-embed-sdk-react.d.ts +1153 -46
  114. package/dist/visual-embed-sdk.d.ts +1161 -46
  115. package/lib/package.json +1 -1
  116. package/lib/src/css-variables.d.ts +95 -0
  117. package/lib/src/css-variables.d.ts.map +1 -1
  118. package/lib/src/embed/app.d.ts +136 -15
  119. package/lib/src/embed/app.d.ts.map +1 -1
  120. package/lib/src/embed/app.js +60 -38
  121. package/lib/src/embed/app.js.map +1 -1
  122. package/lib/src/embed/app.spec.js +420 -150
  123. package/lib/src/embed/app.spec.js.map +1 -1
  124. package/lib/src/embed/conversation.d.ts +230 -1
  125. package/lib/src/embed/conversation.d.ts.map +1 -1
  126. package/lib/src/embed/conversation.js +31 -17
  127. package/lib/src/embed/conversation.js.map +1 -1
  128. package/lib/src/embed/conversation.spec.js +222 -18
  129. package/lib/src/embed/conversation.spec.js.map +1 -1
  130. package/lib/src/embed/events.spec.js +635 -0
  131. package/lib/src/embed/events.spec.js.map +1 -1
  132. package/lib/src/embed/host-events.spec.js +152 -1
  133. package/lib/src/embed/host-events.spec.js.map +1 -1
  134. package/lib/src/embed/hostEventClient/contracts.d.ts +51 -14
  135. package/lib/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  136. package/lib/src/embed/hostEventClient/contracts.js +10 -0
  137. package/lib/src/embed/hostEventClient/contracts.js.map +1 -1
  138. package/lib/src/embed/hostEventClient/host-event-client.d.ts +1 -0
  139. package/lib/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  140. package/lib/src/embed/hostEventClient/host-event-client.js +10 -1
  141. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  142. package/lib/src/embed/hostEventClient/host-event-client.spec.js +218 -0
  143. package/lib/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  144. package/lib/src/embed/hostEventClient/utils.d.ts +2 -0
  145. package/lib/src/embed/hostEventClient/utils.d.ts.map +1 -1
  146. package/lib/src/embed/hostEventClient/utils.js +28 -1
  147. package/lib/src/embed/hostEventClient/utils.js.map +1 -1
  148. package/lib/src/embed/hostEventClient/utils.spec.js +179 -1
  149. package/lib/src/embed/hostEventClient/utils.spec.js.map +1 -1
  150. package/lib/src/embed/liveboard.d.ts +42 -1
  151. package/lib/src/embed/liveboard.d.ts.map +1 -1
  152. package/lib/src/embed/liveboard.js +26 -6
  153. package/lib/src/embed/liveboard.js.map +1 -1
  154. package/lib/src/embed/liveboard.spec.js +83 -2
  155. package/lib/src/embed/liveboard.spec.js.map +1 -1
  156. package/lib/src/embed/spotter-utils.d.ts +8 -1
  157. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  158. package/lib/src/embed/spotter-utils.js +12 -0
  159. package/lib/src/embed/spotter-utils.js.map +1 -1
  160. package/lib/src/embed/spotter-utils.spec.js +62 -1
  161. package/lib/src/embed/spotter-utils.spec.js.map +1 -1
  162. package/lib/src/embed/spotter-viz-utils.d.ts +34 -0
  163. package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -1
  164. package/lib/src/embed/spotter-viz-utils.js.map +1 -1
  165. package/lib/src/embed/spotter-viz-utils.spec.js +17 -0
  166. package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -1
  167. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  168. package/lib/src/embed/ts-embed.js +29 -7
  169. package/lib/src/embed/ts-embed.js.map +1 -1
  170. package/lib/src/embed/ts-embed.spec.js +146 -0
  171. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  172. package/lib/src/errors.d.ts +1 -0
  173. package/lib/src/errors.d.ts.map +1 -1
  174. package/lib/src/errors.js +1 -0
  175. package/lib/src/errors.js.map +1 -1
  176. package/lib/src/index.d.ts +2 -2
  177. package/lib/src/index.d.ts.map +1 -1
  178. package/lib/src/index.js +2 -2
  179. package/lib/src/index.js.map +1 -1
  180. package/lib/src/mixpanel-service.d.ts +4 -0
  181. package/lib/src/mixpanel-service.d.ts.map +1 -1
  182. package/lib/src/mixpanel-service.js +4 -0
  183. package/lib/src/mixpanel-service.js.map +1 -1
  184. package/lib/src/react/util.spec.js +251 -0
  185. package/lib/src/react/util.spec.js.map +1 -1
  186. package/lib/src/types.d.ts +573 -11
  187. package/lib/src/types.d.ts.map +1 -1
  188. package/lib/src/types.js +519 -2
  189. package/lib/src/types.js.map +1 -1
  190. package/lib/src/utils/processData.spec.js +93 -0
  191. package/lib/src/utils/processData.spec.js.map +1 -1
  192. package/lib/src/utils/processTrigger.spec.js +61 -0
  193. package/lib/src/utils/processTrigger.spec.js.map +1 -1
  194. package/lib/src/visual-embed-sdk.d.ts +1161 -46
  195. package/package.json +1 -1
  196. package/src/css-variables.ts +118 -0
  197. package/src/embed/app.spec.ts +530 -183
  198. package/src/embed/app.ts +209 -66
  199. package/src/embed/conversation.spec.ts +275 -21
  200. package/src/embed/conversation.ts +262 -17
  201. package/src/embed/events.spec.ts +757 -1
  202. package/src/embed/host-events.spec.ts +194 -0
  203. package/src/embed/hostEventClient/contracts.ts +57 -14
  204. package/src/embed/hostEventClient/host-event-client.spec.ts +338 -0
  205. package/src/embed/hostEventClient/host-event-client.ts +16 -0
  206. package/src/embed/hostEventClient/utils.spec.ts +204 -0
  207. package/src/embed/hostEventClient/utils.ts +37 -10
  208. package/src/embed/liveboard.spec.ts +108 -2
  209. package/src/embed/liveboard.ts +82 -14
  210. package/src/embed/spotter-utils.spec.ts +68 -1
  211. package/src/embed/spotter-utils.ts +16 -1
  212. package/src/embed/spotter-viz-utils.spec.ts +17 -0
  213. package/src/embed/spotter-viz-utils.ts +34 -0
  214. package/src/embed/ts-embed.spec.ts +186 -0
  215. package/src/embed/ts-embed.ts +30 -4
  216. package/src/errors.ts +1 -0
  217. package/src/index.ts +3 -1
  218. package/src/mixpanel-service.ts +4 -0
  219. package/src/react/util.spec.tsx +255 -0
  220. package/src/types.ts +575 -8
  221. package/src/utils/processData.spec.ts +97 -0
  222. package/src/utils/processTrigger.spec.ts +91 -0
@@ -7,6 +7,7 @@ import {
7
7
  init,
8
8
  AuthType,
9
9
  LiveboardEmbed,
10
+ AppEmbed,
10
11
  HostEvent,
11
12
  EmbedErrorCodes,
12
13
  } from '../index';
@@ -148,6 +149,37 @@ describe('HostEvent.OpenFilter', () => {
148
149
  });
149
150
  });
150
151
 
152
+ // ---------------------------------------------------------------------------
153
+ // 10b – OpenParameter
154
+ // ---------------------------------------------------------------------------
155
+ describe('HostEvent.OpenParameter', () => {
156
+ test('postMessage type is openParameter (camelCase)', async () => {
157
+ mockMessageChannel();
158
+ const { lb, iframe } = await renderLiveboard();
159
+ await executeAfterWait(() => {
160
+ lb.trigger(HostEvent.OpenParameter, { parameter: { parameterId: 'p1' } } as any);
161
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
162
+ expect.objectContaining({ type: 'openParameter' }),
163
+ thoughtSpotHost,
164
+ expect.anything(),
165
+ );
166
+ });
167
+ });
168
+
169
+ test('accepts a payload with only parameterName', async () => {
170
+ mockMessageChannel();
171
+ const { lb, iframe } = await renderLiveboard();
172
+ await executeAfterWait(() => {
173
+ lb.trigger(HostEvent.OpenParameter, { parameter: { parameterName: 'Param 1' } } as any);
174
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
175
+ expect.objectContaining({ type: 'openParameter' }),
176
+ thoughtSpotHost,
177
+ expect.anything(),
178
+ );
179
+ });
180
+ });
181
+ });
182
+
151
183
  // ---------------------------------------------------------------------------
152
184
  // 11 – AddColumns
153
185
  // ---------------------------------------------------------------------------
@@ -1633,3 +1665,165 @@ describe('context parameter forwarded in postMessage payload', () => {
1633
1665
  });
1634
1666
  });
1635
1667
  });
1668
+
1669
+ // ---------------------------------------------------------------------------
1670
+ // SetVisibleVizs — array vizId injection (NOT in the parametrized arrayEvents)
1671
+ // ---------------------------------------------------------------------------
1672
+ describe('HostEvent.SetVisibleVizs — array payload vizId injection', () => {
1673
+ test('vizId is added as a non-indexed property on the array — array.length unchanged', async () => {
1674
+ mockMessageChannel();
1675
+ const { lb, iframe } = await renderLiveboard({ vizId });
1676
+ const arrayPayload = ['viz1', 'viz2'];
1677
+ await executeAfterWait(() => {
1678
+ lb.trigger(HostEvent.SetVisibleVizs, arrayPayload as any);
1679
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1680
+ const sentData = call[0].data;
1681
+ expect((sentData as any).vizId).toBe(vizId);
1682
+ expect(sentData.length).toBe(2);
1683
+ });
1684
+ });
1685
+
1686
+ test('without viewConfig.vizId — array payload is sent as-is (no vizId property)', async () => {
1687
+ mockMessageChannel();
1688
+ const { lb, iframe } = await renderLiveboard(); // no vizId
1689
+ const arrayPayload = ['viz1'];
1690
+ await executeAfterWait(() => {
1691
+ lb.trigger(HostEvent.SetVisibleVizs, arrayPayload as any);
1692
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1693
+ const sentData = call[0].data;
1694
+ expect((sentData as any).vizId).toBeUndefined();
1695
+ });
1696
+ });
1697
+
1698
+ test('returns null and calls handleError when !isRendered', async () => {
1699
+ const lb = unrenderedLiveboard();
1700
+ const result = await lb.trigger(HostEvent.SetVisibleVizs, ['viz1'] as any);
1701
+ expect(result).toBeNull();
1702
+ expect((lb as any).handleError).toHaveBeenCalledWith(
1703
+ expect.objectContaining({ code: EmbedErrorCodes.RENDER_NOT_CALLED }),
1704
+ );
1705
+ });
1706
+ });
1707
+
1708
+ // ---------------------------------------------------------------------------
1709
+ // UpdateRuntimeFilters — AppEmbed path (no vizId injection)
1710
+ // ---------------------------------------------------------------------------
1711
+ describe('HostEvent.UpdateRuntimeFilters — AppEmbed path', () => {
1712
+ async function renderApp() {
1713
+ const app = new AppEmbed(getRootEl(), {
1714
+ frameParams: { width: '100%', height: '100%' },
1715
+ });
1716
+ await app.render();
1717
+ const iframe = getIFrameEl();
1718
+ jest.spyOn(iframe.contentWindow, 'postMessage');
1719
+ return { app, iframe };
1720
+ }
1721
+
1722
+ test('AppEmbed sends payload directly without vizId injection', async () => {
1723
+ mockMessageChannel();
1724
+ const { app, iframe } = await renderApp();
1725
+ const filters = [{ columnName: 'col', operator: 'EQ', values: ['v1'] }];
1726
+ await executeAfterWait(() => {
1727
+ app.trigger(HostEvent.UpdateRuntimeFilters, filters as any);
1728
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1729
+ const sentData = call[0].data;
1730
+ // Array payload with no vizId property injected
1731
+ expect((sentData as any).vizId).toBeUndefined();
1732
+ expect(sentData).toEqual(filters);
1733
+ });
1734
+ });
1735
+ });
1736
+
1737
+ // ---------------------------------------------------------------------------
1738
+ // Present — vizId injection on LiveboardEmbed
1739
+ // ---------------------------------------------------------------------------
1740
+ describe('HostEvent.Present — vizId injection on LiveboardEmbed', () => {
1741
+ test('viewConfig.vizId is injected into the Present payload', async () => {
1742
+ mockMessageChannel();
1743
+ const { lb, iframe } = await renderLiveboard({ vizId });
1744
+ await executeAfterWait(() => {
1745
+ lb.trigger(HostEvent.Present, {} as any);
1746
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
1747
+ expect.objectContaining({
1748
+ data: expect.objectContaining({ vizId }),
1749
+ }),
1750
+ thoughtSpotHost,
1751
+ expect.anything(),
1752
+ );
1753
+ });
1754
+ });
1755
+ });
1756
+
1757
+ // ---------------------------------------------------------------------------
1758
+ // DownloadAsCsv — additional cases
1759
+ // ---------------------------------------------------------------------------
1760
+ describe('HostEvent.DownloadAsCsv — additional cases', () => {
1761
+ test('viewConfig.vizId overwrites an explicit payload vizId', async () => {
1762
+ mockMessageChannel();
1763
+ const { lb, iframe } = await renderLiveboard({ vizId });
1764
+ await executeAfterWait(() => {
1765
+ lb.trigger(HostEvent.DownloadAsCsv, { vizId: 'explicit-id' } as any);
1766
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1767
+ const sentData = call[0].data;
1768
+ expect(sentData.vizId).toBe(vizId); // viewConfig.vizId wins
1769
+ });
1770
+ });
1771
+
1772
+ test('returns null and calls handleError when !isRendered', async () => {
1773
+ const lb = unrenderedLiveboard();
1774
+ const result = await lb.trigger(HostEvent.DownloadAsCsv, {} as any);
1775
+ expect(result).toBeNull();
1776
+ expect((lb as any).handleError).toHaveBeenCalledWith(
1777
+ expect.objectContaining({ code: EmbedErrorCodes.RENDER_NOT_CALLED }),
1778
+ );
1779
+ });
1780
+
1781
+ test('returns null and logs debug when !iFrame', async () => {
1782
+ const lb = new LiveboardEmbed(getRootEl(), {
1783
+ frameParams: { width: '100%', height: '100%' },
1784
+ liveboardId,
1785
+ });
1786
+ (lb as any).isRendered = true;
1787
+ const result = await lb.trigger(HostEvent.DownloadAsCsv, {} as any);
1788
+ expect(result).toBeNull();
1789
+ });
1790
+ });
1791
+
1792
+ // ---------------------------------------------------------------------------
1793
+ // Spotter share-conversation HostEvents
1794
+ // ---------------------------------------------------------------------------
1795
+ describe('Spotter share-conversation HostEvents', () => {
1796
+ const shareEvents: Array<[string, HostEvent, string, any]> = [
1797
+ ['ShareSpotterConversation', HostEvent.ShareSpotterConversation, 'ShareSpotterConversation', { conversationId: 'abc' }],
1798
+ ['CloseSpotterShareConversation', HostEvent.CloseSpotterShareConversation, 'CloseSpotterShareConversation', {}],
1799
+ ['ExitSpotterSharedConversation', HostEvent.ExitSpotterSharedConversation, 'ExitSpotterSharedConversation', {}],
1800
+ ];
1801
+
1802
+ test.each(shareEvents)(
1803
+ '%s: postMessage type is the matching string value',
1804
+ async (name, event, typeString, payload) => {
1805
+ mockMessageChannel();
1806
+ const { lb, iframe } = await renderLiveboard();
1807
+ await executeAfterWait(() => {
1808
+ lb.trigger(event, payload as any);
1809
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
1810
+ expect.objectContaining({ type: typeString }),
1811
+ thoughtSpotHost,
1812
+ expect.anything(),
1813
+ );
1814
+ });
1815
+ },
1816
+ );
1817
+
1818
+ test.each(shareEvents)(
1819
+ '%s: returns null and calls handleError when !isRendered',
1820
+ async (name, event, typeString, payload) => {
1821
+ const lb = unrenderedLiveboard();
1822
+ const result = await lb.trigger(event, payload as any);
1823
+ expect(result).toBeNull();
1824
+ expect((lb as any).handleError).toHaveBeenCalledWith(
1825
+ expect.objectContaining({ code: EmbedErrorCodes.RENDER_NOT_CALLED }),
1826
+ );
1827
+ },
1828
+ );
1829
+ });
@@ -7,6 +7,49 @@ export interface LiveboardTab {
7
7
  [key: string]: any;
8
8
  }
9
9
 
10
+ export interface LiveboardGroup {
11
+ id: string;
12
+ name: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ /**
17
+ * Levels at which a filter or parameter can be applied.
18
+ */
19
+ export enum ApplicabilityLevel {
20
+ Liveboard = 'LIVEBOARD',
21
+ Tab = 'TAB',
22
+ Group = 'GROUP',
23
+ }
24
+
25
+ /**
26
+ * Scopes a filter or parameter to a specific target.
27
+ * At `LIVEBOARD` level the filter applies to the whole Liveboard, so `targetId`
28
+ * is not required.
29
+ */
30
+ export interface Applicability {
31
+ level: ApplicabilityLevel;
32
+ targetId?: string;
33
+ }
34
+
35
+ export interface FilterUpdate {
36
+ column: string;
37
+ oper: string;
38
+ values: string[];
39
+ type?: string;
40
+ applicability?: Applicability;
41
+ }
42
+
43
+ export interface LiveboardFilter {
44
+ applicability?: Applicability;
45
+ [key: string]: any;
46
+ }
47
+
48
+ export interface LiveboardParameter {
49
+ applicability?: Applicability;
50
+ [key: string]: any;
51
+ }
52
+
10
53
  export enum UIPassthroughEvent {
11
54
  PinAnswerToLiveboard = 'addVizToPinboard',
12
55
  SaveAnswer = 'saveAnswer',
@@ -23,6 +66,7 @@ export enum UIPassthroughEvent {
23
66
  GetParameters = 'getParameters',
24
67
  GetTML = 'getTML',
25
68
  GetTabs = 'getTabs',
69
+ GetGroups = 'getGroups',
26
70
  GetExportRequestForCurrentPinboard = 'getExportRequestForCurrentPinboard',
27
71
  }
28
72
 
@@ -103,7 +147,7 @@ export type UIPassthroughContractBase = {
103
147
  vizId?: string;
104
148
  };
105
149
  response: {
106
- liveboardFilters: Record<string, any>[];
150
+ liveboardFilters: LiveboardFilter[];
107
151
  runtimeFilters: RuntimeFilter[];
108
152
  };
109
153
  };
@@ -116,7 +160,7 @@ export type UIPassthroughContractBase = {
116
160
  [UIPassthroughEvent.GetParameters]: {
117
161
  request: Record<string, never>;
118
162
  response: {
119
- parameters: Record<string, any>[];
163
+ parameters: LiveboardParameter[];
120
164
  };
121
165
  };
122
166
  [UIPassthroughEvent.GetTML]: {
@@ -134,6 +178,14 @@ export type UIPassthroughContractBase = {
134
178
  Tabs: LiveboardTab[];
135
179
  };
136
180
  };
181
+ [UIPassthroughEvent.GetGroups]: {
182
+ request: Record<string, never>;
183
+ response: {
184
+ orderedGroupIds: string[];
185
+ numberOfGroups: number;
186
+ Groups: LiveboardGroup[];
187
+ };
188
+ };
137
189
  [UIPassthroughEvent.GetExportRequestForCurrentPinboard]: {
138
190
  request: Record<string, never>;
139
191
  response: {
@@ -143,18 +195,8 @@ export type UIPassthroughContractBase = {
143
195
  };
144
196
  [UIPassthroughEvent.UpdateFilters]: {
145
197
  request: {
146
- filter?: {
147
- column: string;
148
- oper: string;
149
- values: string[];
150
- type?: string;
151
- };
152
- filters?: {
153
- column: string;
154
- oper: string;
155
- values: string[];
156
- type?: string;
157
- }[];
198
+ filter?: FilterUpdate;
199
+ filters?: FilterUpdate[];
158
200
  };
159
201
  response: unknown;
160
202
  };
@@ -198,6 +240,7 @@ export type EmbedApiHostEventMapping = {
198
240
  [HostEvent.GetParameters]: UIPassthroughEvent.GetParameters;
199
241
  [HostEvent.GetTML]: UIPassthroughEvent.GetTML;
200
242
  [HostEvent.GetTabs]: UIPassthroughEvent.GetTabs;
243
+ [HostEvent.GetGroups]: UIPassthroughEvent.GetGroups;
201
244
  [HostEvent.getExportRequestForCurrentPinboard]: UIPassthroughEvent.GetExportRequestForCurrentPinboard;
202
245
  }
203
246
 
@@ -311,6 +311,82 @@ describe('HostEventClient', () => {
311
311
  );
312
312
  });
313
313
 
314
+ it('should dispatch UpdateParameters over the legacy channel', async () => {
315
+ const { client, mockIframe } = createHostEventClient();
316
+ const payload = [
317
+ { name: 'param1', value: 10, applicability: { level: 'TAB', targetId: 'tab-guid-1' } },
318
+ ] as any;
319
+ mockProcessTrigger.mockResolvedValueOnce({ success: true });
320
+
321
+ const result = await client.triggerHostEvent(HostEvent.UpdateParameters, payload);
322
+
323
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(1);
324
+ expect(mockProcessTrigger).toHaveBeenCalledWith(
325
+ mockIframe,
326
+ HostEvent.UpdateParameters,
327
+ mockThoughtSpotHost,
328
+ payload,
329
+ undefined,
330
+ );
331
+ expect(result).toEqual({ success: true });
332
+ });
333
+
334
+ it('should forward UpdateParameters with null applicability', async () => {
335
+ const { client, mockIframe } = createHostEventClient();
336
+ const payload = [{ name: 'param1', value: 10, applicability: null }] as any;
337
+ mockProcessTrigger.mockResolvedValueOnce({ success: true });
338
+
339
+ await client.triggerHostEvent(HostEvent.UpdateParameters, payload);
340
+
341
+ expect(mockProcessTrigger).toHaveBeenCalledWith(
342
+ mockIframe,
343
+ HostEvent.UpdateParameters,
344
+ mockThoughtSpotHost,
345
+ payload,
346
+ undefined,
347
+ );
348
+ });
349
+
350
+ it('should throw when UpdateParameters entry has invalid applicability', async () => {
351
+ const { client } = createHostEventClient();
352
+ const invalidPayload = [
353
+ { name: 'param1', value: 10, applicability: { level: 'TAB' } }, // missing targetId
354
+ ] as any;
355
+
356
+ await expect(client.triggerHostEvent(HostEvent.UpdateParameters, invalidPayload))
357
+ .rejects.toThrow('UpdateParameters received an invalid applicability');
358
+ expect(mockProcessTrigger).not.toHaveBeenCalled();
359
+ });
360
+
361
+ it('should pass context to UpdateParameters event', async () => {
362
+ const { client, mockIframe } = createHostEventClient();
363
+ const payload = [{ name: 'param1', value: 10 }] as any;
364
+ const context = { vizId: 'viz-1' } as any;
365
+ mockProcessTrigger.mockResolvedValueOnce({ success: true });
366
+
367
+ await client.triggerHostEvent(HostEvent.UpdateParameters, payload, context);
368
+
369
+ expect(mockProcessTrigger).toHaveBeenCalledWith(
370
+ mockIframe,
371
+ HostEvent.UpdateParameters,
372
+ mockThoughtSpotHost,
373
+ payload,
374
+ context,
375
+ );
376
+ });
377
+
378
+ it('should route GetGroups through passthrough and return data', async () => {
379
+ const { client } = createHostEventClient();
380
+ const mockResponse = [{ value: { orderedGroupIds: ['g1', 'g2'], numberOfGroups: 2, Groups: [] as any[] } }];
381
+ mockProcessTrigger
382
+ .mockResolvedValueOnce(mockGetAvailablePassthroughs())
383
+ .mockResolvedValueOnce(mockResponse);
384
+
385
+ const result = await client.triggerHostEvent(HostEvent.GetGroups, {});
386
+
387
+ expect(result).toEqual({ orderedGroupIds: ['g1', 'g2'], numberOfGroups: 2, Groups: [] });
388
+ });
389
+
314
390
  it('should throw when DrillDown payload has no valid points', async () => {
315
391
  const { client } = createHostEventClient();
316
392
  const invalidPayload = {} as any;
@@ -816,4 +892,266 @@ describe('HostEventClient', () => {
816
892
  expect(result).toEqual({ ok: true });
817
893
  });
818
894
  });
895
+
896
+ describe('availablePassthroughKeysCache reuse', () => {
897
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
898
+
899
+ it('GetFilters: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
900
+ const { client } = createHostEventClient();
901
+ const filterResponse = [{ value: { liveboardFilters: [] as any[], runtimeFilters: [] as any[] } }];
902
+ mockProcessTrigger
903
+ .mockResolvedValueOnce(allKeys())
904
+ .mockResolvedValueOnce(filterResponse)
905
+ .mockResolvedValueOnce(filterResponse);
906
+
907
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
908
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
909
+
910
+ // 3 calls total: 1 GetAvailableUIPassthroughs + 2 GetFilters
911
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
912
+ expect(mockProcessTrigger).not.toHaveBeenNthCalledWith(
913
+ 3,
914
+ expect.anything(),
915
+ HostEvent.UIPassthrough,
916
+ mockThoughtSpotHost,
917
+ expect.objectContaining({ type: UIPassthroughEvent.GetAvailableUIPassthroughs }),
918
+ undefined,
919
+ );
920
+ });
921
+
922
+ it('GetTabs: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
923
+ const { client } = createHostEventClient();
924
+ const tabResponse = [{ value: { orderedTabIds: [] as any[], numberOfTabs: 0, Tabs: [] as any[] } }];
925
+ mockProcessTrigger
926
+ .mockResolvedValueOnce(allKeys())
927
+ .mockResolvedValueOnce(tabResponse)
928
+ .mockResolvedValueOnce(tabResponse);
929
+
930
+ await client.triggerHostEvent(HostEvent.GetTabs, {});
931
+ await client.triggerHostEvent(HostEvent.GetTabs, {});
932
+
933
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
934
+ });
935
+
936
+ it('GetParameters: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
937
+ const { client } = createHostEventClient();
938
+ const paramResponse = [{ value: { parameters: [] as any[] } }];
939
+ mockProcessTrigger
940
+ .mockResolvedValueOnce(allKeys())
941
+ .mockResolvedValueOnce(paramResponse)
942
+ .mockResolvedValueOnce(paramResponse);
943
+
944
+ await client.triggerHostEvent(HostEvent.GetParameters, {});
945
+ await client.triggerHostEvent(HostEvent.GetParameters, {});
946
+
947
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
948
+ });
949
+
950
+ it('GetAnswerSession: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
951
+ const { client } = createHostEventClient();
952
+ const sessionResponse = [{ value: { session: 's1' } }];
953
+ mockProcessTrigger
954
+ .mockResolvedValueOnce(allKeys())
955
+ .mockResolvedValueOnce(sessionResponse)
956
+ .mockResolvedValueOnce(sessionResponse);
957
+
958
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, {});
959
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, {});
960
+
961
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
962
+ });
963
+
964
+ it('UIPassthrough direct trigger does not populate the cache', async () => {
965
+ const { client } = createHostEventClient();
966
+ mockProcessTrigger.mockResolvedValue([{ value: { result: 'ok' } }]);
967
+
968
+ await client.triggerHostEvent(HostEvent.UIPassthrough, {});
969
+ // Cache not populated — next GetFilters call must still fetch keys
970
+ mockProcessTrigger.mockResolvedValueOnce(allKeys());
971
+ mockProcessTrigger.mockResolvedValueOnce([{ value: { liveboardFilters: [], runtimeFilters: [] as any[] } }]);
972
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
973
+
974
+ // UIPassthrough call + GetAvailableUIPassthroughs + GetFilters = 3
975
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
976
+ });
977
+
978
+ it('GetAnswerSession: catch in getAvailableUIPassthroughKeys does not cache, so next call retries GetAvailableUIPassthroughs', async () => {
979
+ const { client } = createHostEventClient();
980
+ mockProcessTrigger.mockReset();
981
+
982
+ // Flow per triggerHostEvent call when GetAvailableUIPassthroughs rejects:
983
+ // 1. getAvailableUIPassthroughKeys → processTrigger(UIPassthrough, GetAvailableUIPassthroughs) → rejects → returns []
984
+ // 2. keys=[] so getDataWithPassthroughFallback is called →
985
+ // triggerUIPassthroughApi(GetAnswerSession) → processTrigger(UIPassthrough) → null → hostEventFallback called
986
+ // 3. hostEventFallback → processTrigger(GetAnswerSession) → legacy result
987
+
988
+ // First triggerHostEvent: 3 calls
989
+ mockProcessTrigger.mockRejectedValueOnce(new Error('network error')); // call 1
990
+ mockProcessTrigger.mockResolvedValueOnce(null); // call 2
991
+ mockProcessTrigger.mockResolvedValueOnce({ session: 'leg1' }); // call 3
992
+
993
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '1' });
994
+
995
+ // Second triggerHostEvent: cache was NOT set, so GetAvailableUIPassthroughs is retried: 3 more calls
996
+ mockProcessTrigger.mockRejectedValueOnce(new Error('network error')); // call 4
997
+ mockProcessTrigger.mockResolvedValueOnce(null); // call 5
998
+ mockProcessTrigger.mockResolvedValueOnce({ session: 'leg2' }); // call 6
999
+
1000
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '2' });
1001
+
1002
+ // 6 total calls: GetAvailableUIPassthroughs was called TWICE (cache not set after catch)
1003
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(6);
1004
+ const availableCallsCount = mockProcessTrigger.mock.calls.filter(
1005
+ (call) => call[3]?.type === UIPassthroughEvent.GetAvailableUIPassthroughs,
1006
+ ).length;
1007
+ expect(availableCallsCount).toBe(2);
1008
+ });
1009
+ });
1010
+
1011
+ describe('GetTML edge cases', () => {
1012
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
1013
+
1014
+ it('includeNonExecutedSearchTokens flag is forwarded in UIPassthrough parameters', async () => {
1015
+ const { client, mockIframe } = createHostEventClient();
1016
+ mockProcessTrigger
1017
+ .mockResolvedValueOnce(allKeys())
1018
+ .mockResolvedValueOnce([{ value: { tml: 'data' } }]);
1019
+
1020
+ await client.triggerHostEvent(HostEvent.GetTML, { includeNonExecutedSearchTokens: true } as any);
1021
+
1022
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1023
+ 2,
1024
+ mockIframe,
1025
+ HostEvent.UIPassthrough,
1026
+ mockThoughtSpotHost,
1027
+ {
1028
+ type: UIPassthroughEvent.GetTML,
1029
+ parameters: { includeNonExecutedSearchTokens: true },
1030
+ },
1031
+ undefined,
1032
+ );
1033
+ });
1034
+
1035
+ it('value.error (singular) throws — distinct from value.errors', async () => {
1036
+ const { client } = createHostEventClient();
1037
+ mockProcessTrigger
1038
+ .mockResolvedValueOnce(allKeys())
1039
+ .mockResolvedValueOnce([{ value: { error: 'TML fetch failed' } }]);
1040
+
1041
+ await expect(client.triggerHostEvent(HostEvent.GetTML, {}))
1042
+ .rejects.toThrow('TML fetch failed');
1043
+ });
1044
+
1045
+ it('GetTML key absent from non-empty keys array falls back to legacy hostEventFallback', async () => {
1046
+ const { client, mockIframe } = createHostEventClient();
1047
+ // Return keys that do NOT include GetTML
1048
+ mockProcessTrigger
1049
+ .mockResolvedValueOnce([{ value: { keys: [UIPassthroughEvent.GetFilters] } }])
1050
+ .mockResolvedValueOnce({ answer: { search_query: 'revenue' } });
1051
+
1052
+ const result = await client.triggerHostEvent(HostEvent.GetTML, {});
1053
+
1054
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1055
+ 2,
1056
+ mockIframe,
1057
+ HostEvent.GetTML,
1058
+ mockThoughtSpotHost,
1059
+ {},
1060
+ undefined,
1061
+ );
1062
+ expect(result).toEqual({ answer: { search_query: 'revenue' } });
1063
+ });
1064
+ });
1065
+
1066
+ describe('SaveAnswer key-presence edge cases', () => {
1067
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
1068
+ const saveResponse = [{
1069
+ value: {
1070
+ saveResponse: {
1071
+ data: {
1072
+ Answer__save: {
1073
+ answer: { id: 'ans123' },
1074
+ },
1075
+ },
1076
+ },
1077
+ },
1078
+ refId: 'viz1',
1079
+ }];
1080
+
1081
+ it('name+description both empty string route through passthrough, not Save fallback', async () => {
1082
+ const { client, mockIframe } = createHostEventClient();
1083
+ mockProcessTrigger
1084
+ .mockResolvedValueOnce(allKeys())
1085
+ .mockResolvedValueOnce(saveResponse);
1086
+
1087
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { name: '', description: '' } as any);
1088
+
1089
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1090
+ 2,
1091
+ mockIframe,
1092
+ HostEvent.UIPassthrough,
1093
+ mockThoughtSpotHost,
1094
+ { type: UIPassthroughEvent.SaveAnswer, parameters: { name: '', description: '' } },
1095
+ undefined,
1096
+ );
1097
+ });
1098
+
1099
+ it('description: undefined (key present) routes through passthrough, not Save fallback', async () => {
1100
+ const { client, mockIframe } = createHostEventClient();
1101
+ mockProcessTrigger
1102
+ .mockResolvedValueOnce(allKeys())
1103
+ .mockResolvedValueOnce(saveResponse);
1104
+
1105
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { name: 'Test', description: undefined } as any);
1106
+
1107
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1108
+ 2,
1109
+ mockIframe,
1110
+ HostEvent.UIPassthrough,
1111
+ mockThoughtSpotHost,
1112
+ { type: UIPassthroughEvent.SaveAnswer, parameters: { name: 'Test', description: undefined } },
1113
+ undefined,
1114
+ );
1115
+ });
1116
+
1117
+ it('name key absent falls back to HostEvent.Save', async () => {
1118
+ const { client, mockIframe } = createHostEventClient();
1119
+ mockProcessTrigger
1120
+ .mockResolvedValueOnce(allKeys())
1121
+ .mockResolvedValueOnce([saveResponse[0].value]);
1122
+
1123
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { description: 'desc only' } as any);
1124
+
1125
+ expect(mockProcessTrigger).toHaveBeenCalledWith(
1126
+ mockIframe,
1127
+ HostEvent.Save,
1128
+ mockThoughtSpotHost,
1129
+ { description: 'desc only' },
1130
+ undefined,
1131
+ );
1132
+ });
1133
+
1134
+ it('answerId is undefined when saveResponse.data.Answer__save.answer.id is missing', async () => {
1135
+ const { client } = createHostEventClient();
1136
+ const noIdSaveResponse = [{
1137
+ value: {
1138
+ saveResponse: {
1139
+ data: {
1140
+ Answer__save: {
1141
+ answer: {},
1142
+ },
1143
+ },
1144
+ },
1145
+ },
1146
+ refId: 'viz1',
1147
+ }];
1148
+ mockProcessTrigger
1149
+ .mockResolvedValueOnce(allKeys())
1150
+ .mockResolvedValueOnce(noIdSaveResponse);
1151
+
1152
+ const result = await client.triggerHostEvent(HostEvent.SaveAnswer, { name: 'Test', description: 'Desc' } as any);
1153
+
1154
+ expect(result.answerId).toBeUndefined();
1155
+ });
1156
+ });
819
1157
  });