@thoughtspot/visual-embed-sdk 1.50.0 → 1.50.1

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 (188) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/css-variables.d.ts +59 -0
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +31 -2
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js +13 -2
  7. package/cjs/src/embed/app.js.map +1 -1
  8. package/cjs/src/embed/app.spec.js +101 -0
  9. package/cjs/src/embed/app.spec.js.map +1 -1
  10. package/cjs/src/embed/conversation.d.ts +26 -0
  11. package/cjs/src/embed/conversation.d.ts.map +1 -1
  12. package/cjs/src/embed/conversation.js +12 -2
  13. package/cjs/src/embed/conversation.js.map +1 -1
  14. package/cjs/src/embed/conversation.spec.js +23 -0
  15. package/cjs/src/embed/conversation.spec.js.map +1 -1
  16. package/cjs/src/embed/events.spec.js +593 -0
  17. package/cjs/src/embed/events.spec.js.map +1 -1
  18. package/cjs/src/embed/host-events.spec.js +106 -0
  19. package/cjs/src/embed/host-events.spec.js.map +1 -1
  20. package/cjs/src/embed/hostEventClient/contracts.d.ts +36 -14
  21. package/cjs/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  22. package/cjs/src/embed/hostEventClient/contracts.js +10 -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 +8 -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 +209 -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 +6 -0
  37. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  38. package/cjs/src/embed/liveboard.js.map +1 -1
  39. package/cjs/src/embed/spotter-viz-utils.d.ts +34 -0
  40. package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -1
  41. package/cjs/src/embed/spotter-viz-utils.js.map +1 -1
  42. package/cjs/src/embed/spotter-viz-utils.spec.js +17 -0
  43. package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -1
  44. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  45. package/cjs/src/embed/ts-embed.js +14 -7
  46. package/cjs/src/embed/ts-embed.js.map +1 -1
  47. package/cjs/src/embed/ts-embed.spec.js +146 -0
  48. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  49. package/cjs/src/errors.d.ts +1 -0
  50. package/cjs/src/errors.d.ts.map +1 -1
  51. package/cjs/src/errors.js +1 -0
  52. package/cjs/src/errors.js.map +1 -1
  53. package/cjs/src/index.d.ts +2 -2
  54. package/cjs/src/index.d.ts.map +1 -1
  55. package/cjs/src/index.js +3 -2
  56. package/cjs/src/index.js.map +1 -1
  57. package/cjs/src/react/util.spec.js +251 -0
  58. package/cjs/src/react/util.spec.js.map +1 -1
  59. package/cjs/src/types.d.ts +119 -2
  60. package/cjs/src/types.d.ts.map +1 -1
  61. package/cjs/src/types.js +97 -0
  62. package/cjs/src/types.js.map +1 -1
  63. package/cjs/src/utils/processData.spec.js +93 -0
  64. package/cjs/src/utils/processData.spec.js.map +1 -1
  65. package/cjs/src/utils/processTrigger.spec.js +61 -0
  66. package/cjs/src/utils/processTrigger.spec.js.map +1 -1
  67. package/dist/{index-B5xoN6Cp.js → index-fCne7kmU.js} +1 -1
  68. package/dist/src/css-variables.d.ts +59 -0
  69. package/dist/src/css-variables.d.ts.map +1 -1
  70. package/dist/src/embed/app.d.ts +31 -2
  71. package/dist/src/embed/app.d.ts.map +1 -1
  72. package/dist/src/embed/conversation.d.ts +26 -0
  73. package/dist/src/embed/conversation.d.ts.map +1 -1
  74. package/dist/src/embed/hostEventClient/contracts.d.ts +36 -14
  75. package/dist/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  76. package/dist/src/embed/hostEventClient/host-event-client.d.ts +1 -0
  77. package/dist/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  78. package/dist/src/embed/hostEventClient/utils.d.ts +2 -0
  79. package/dist/src/embed/hostEventClient/utils.d.ts.map +1 -1
  80. package/dist/src/embed/liveboard.d.ts +6 -0
  81. package/dist/src/embed/liveboard.d.ts.map +1 -1
  82. package/dist/src/embed/spotter-viz-utils.d.ts +34 -0
  83. package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -1
  84. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  85. package/dist/src/errors.d.ts +1 -0
  86. package/dist/src/errors.d.ts.map +1 -1
  87. package/dist/src/index.d.ts +2 -2
  88. package/dist/src/index.d.ts.map +1 -1
  89. package/dist/src/types.d.ts +119 -2
  90. package/dist/src/types.d.ts.map +1 -1
  91. package/dist/tsembed-react.es.js +389 -84
  92. package/dist/tsembed-react.js +388 -83
  93. package/dist/tsembed.es.js +390 -85
  94. package/dist/tsembed.js +388 -83
  95. package/dist/visual-embed-sdk-react-full.d.ts +302 -17
  96. package/dist/visual-embed-sdk-react.d.ts +302 -17
  97. package/dist/visual-embed-sdk.d.ts +306 -17
  98. package/lib/package.json +1 -1
  99. package/lib/src/css-variables.d.ts +59 -0
  100. package/lib/src/css-variables.d.ts.map +1 -1
  101. package/lib/src/embed/app.d.ts +31 -2
  102. package/lib/src/embed/app.d.ts.map +1 -1
  103. package/lib/src/embed/app.js +13 -2
  104. package/lib/src/embed/app.js.map +1 -1
  105. package/lib/src/embed/app.spec.js +101 -0
  106. package/lib/src/embed/app.spec.js.map +1 -1
  107. package/lib/src/embed/conversation.d.ts +26 -0
  108. package/lib/src/embed/conversation.d.ts.map +1 -1
  109. package/lib/src/embed/conversation.js +11 -1
  110. package/lib/src/embed/conversation.js.map +1 -1
  111. package/lib/src/embed/conversation.spec.js +24 -1
  112. package/lib/src/embed/conversation.spec.js.map +1 -1
  113. package/lib/src/embed/events.spec.js +593 -0
  114. package/lib/src/embed/events.spec.js.map +1 -1
  115. package/lib/src/embed/host-events.spec.js +107 -1
  116. package/lib/src/embed/host-events.spec.js.map +1 -1
  117. package/lib/src/embed/hostEventClient/contracts.d.ts +36 -14
  118. package/lib/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  119. package/lib/src/embed/hostEventClient/contracts.js +9 -0
  120. package/lib/src/embed/hostEventClient/contracts.js.map +1 -1
  121. package/lib/src/embed/hostEventClient/host-event-client.d.ts +1 -0
  122. package/lib/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  123. package/lib/src/embed/hostEventClient/host-event-client.js +9 -1
  124. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  125. package/lib/src/embed/hostEventClient/host-event-client.spec.js +209 -0
  126. package/lib/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  127. package/lib/src/embed/hostEventClient/utils.d.ts +2 -0
  128. package/lib/src/embed/hostEventClient/utils.d.ts.map +1 -1
  129. package/lib/src/embed/hostEventClient/utils.js +28 -1
  130. package/lib/src/embed/hostEventClient/utils.js.map +1 -1
  131. package/lib/src/embed/hostEventClient/utils.spec.js +179 -1
  132. package/lib/src/embed/hostEventClient/utils.spec.js.map +1 -1
  133. package/lib/src/embed/liveboard.d.ts +6 -0
  134. package/lib/src/embed/liveboard.d.ts.map +1 -1
  135. package/lib/src/embed/liveboard.js.map +1 -1
  136. package/lib/src/embed/spotter-viz-utils.d.ts +34 -0
  137. package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -1
  138. package/lib/src/embed/spotter-viz-utils.js.map +1 -1
  139. package/lib/src/embed/spotter-viz-utils.spec.js +17 -0
  140. package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -1
  141. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  142. package/lib/src/embed/ts-embed.js +14 -7
  143. package/lib/src/embed/ts-embed.js.map +1 -1
  144. package/lib/src/embed/ts-embed.spec.js +146 -0
  145. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  146. package/lib/src/errors.d.ts +1 -0
  147. package/lib/src/errors.d.ts.map +1 -1
  148. package/lib/src/errors.js +1 -0
  149. package/lib/src/errors.js.map +1 -1
  150. package/lib/src/index.d.ts +2 -2
  151. package/lib/src/index.d.ts.map +1 -1
  152. package/lib/src/index.js +2 -2
  153. package/lib/src/index.js.map +1 -1
  154. package/lib/src/react/util.spec.js +251 -0
  155. package/lib/src/react/util.spec.js.map +1 -1
  156. package/lib/src/types.d.ts +119 -2
  157. package/lib/src/types.d.ts.map +1 -1
  158. package/lib/src/types.js +97 -0
  159. package/lib/src/types.js.map +1 -1
  160. package/lib/src/utils/processData.spec.js +93 -0
  161. package/lib/src/utils/processData.spec.js.map +1 -1
  162. package/lib/src/utils/processTrigger.spec.js +61 -0
  163. package/lib/src/utils/processTrigger.spec.js.map +1 -1
  164. package/lib/src/visual-embed-sdk.d.ts +306 -17
  165. package/package.json +1 -1
  166. package/src/css-variables.ts +73 -0
  167. package/src/embed/app.spec.ts +140 -0
  168. package/src/embed/app.ts +38 -2
  169. package/src/embed/conversation.spec.ts +32 -1
  170. package/src/embed/conversation.ts +29 -0
  171. package/src/embed/events.spec.ts +705 -1
  172. package/src/embed/host-events.spec.ts +124 -0
  173. package/src/embed/hostEventClient/contracts.ts +41 -14
  174. package/src/embed/hostEventClient/host-event-client.spec.ts +326 -0
  175. package/src/embed/hostEventClient/host-event-client.ts +15 -0
  176. package/src/embed/hostEventClient/utils.spec.ts +204 -0
  177. package/src/embed/hostEventClient/utils.ts +37 -10
  178. package/src/embed/liveboard.ts +6 -0
  179. package/src/embed/spotter-viz-utils.spec.ts +17 -0
  180. package/src/embed/spotter-viz-utils.ts +34 -0
  181. package/src/embed/ts-embed.spec.ts +186 -0
  182. package/src/embed/ts-embed.ts +12 -4
  183. package/src/errors.ts +1 -0
  184. package/src/index.ts +2 -1
  185. package/src/react/util.spec.tsx +255 -0
  186. package/src/types.ts +119 -0
  187. package/src/utils/processData.spec.ts +97 -0
  188. 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';
@@ -1633,3 +1634,126 @@ describe('context parameter forwarded in postMessage payload', () => {
1633
1634
  });
1634
1635
  });
1635
1636
  });
1637
+
1638
+ // ---------------------------------------------------------------------------
1639
+ // SetVisibleVizs — array vizId injection (NOT in the parametrized arrayEvents)
1640
+ // ---------------------------------------------------------------------------
1641
+ describe('HostEvent.SetVisibleVizs — array payload vizId injection', () => {
1642
+ test('vizId is added as a non-indexed property on the array — array.length unchanged', async () => {
1643
+ mockMessageChannel();
1644
+ const { lb, iframe } = await renderLiveboard({ vizId });
1645
+ const arrayPayload = ['viz1', 'viz2'];
1646
+ await executeAfterWait(() => {
1647
+ lb.trigger(HostEvent.SetVisibleVizs, arrayPayload as any);
1648
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1649
+ const sentData = call[0].data;
1650
+ expect((sentData as any).vizId).toBe(vizId);
1651
+ expect(sentData.length).toBe(2);
1652
+ });
1653
+ });
1654
+
1655
+ test('without viewConfig.vizId — array payload is sent as-is (no vizId property)', async () => {
1656
+ mockMessageChannel();
1657
+ const { lb, iframe } = await renderLiveboard(); // no vizId
1658
+ const arrayPayload = ['viz1'];
1659
+ await executeAfterWait(() => {
1660
+ lb.trigger(HostEvent.SetVisibleVizs, arrayPayload as any);
1661
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1662
+ const sentData = call[0].data;
1663
+ expect((sentData as any).vizId).toBeUndefined();
1664
+ });
1665
+ });
1666
+
1667
+ test('returns null and calls handleError when !isRendered', async () => {
1668
+ const lb = unrenderedLiveboard();
1669
+ const result = await lb.trigger(HostEvent.SetVisibleVizs, ['viz1'] as any);
1670
+ expect(result).toBeNull();
1671
+ expect((lb as any).handleError).toHaveBeenCalledWith(
1672
+ expect.objectContaining({ code: EmbedErrorCodes.RENDER_NOT_CALLED }),
1673
+ );
1674
+ });
1675
+ });
1676
+
1677
+ // ---------------------------------------------------------------------------
1678
+ // UpdateRuntimeFilters — AppEmbed path (no vizId injection)
1679
+ // ---------------------------------------------------------------------------
1680
+ describe('HostEvent.UpdateRuntimeFilters — AppEmbed path', () => {
1681
+ async function renderApp() {
1682
+ const app = new AppEmbed(getRootEl(), {
1683
+ frameParams: { width: '100%', height: '100%' },
1684
+ });
1685
+ await app.render();
1686
+ const iframe = getIFrameEl();
1687
+ jest.spyOn(iframe.contentWindow, 'postMessage');
1688
+ return { app, iframe };
1689
+ }
1690
+
1691
+ test('AppEmbed sends payload directly without vizId injection', async () => {
1692
+ mockMessageChannel();
1693
+ const { app, iframe } = await renderApp();
1694
+ const filters = [{ columnName: 'col', operator: 'EQ', values: ['v1'] }];
1695
+ await executeAfterWait(() => {
1696
+ app.trigger(HostEvent.UpdateRuntimeFilters, filters as any);
1697
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1698
+ const sentData = call[0].data;
1699
+ // Array payload with no vizId property injected
1700
+ expect((sentData as any).vizId).toBeUndefined();
1701
+ expect(sentData).toEqual(filters);
1702
+ });
1703
+ });
1704
+ });
1705
+
1706
+ // ---------------------------------------------------------------------------
1707
+ // Present — vizId injection on LiveboardEmbed
1708
+ // ---------------------------------------------------------------------------
1709
+ describe('HostEvent.Present — vizId injection on LiveboardEmbed', () => {
1710
+ test('viewConfig.vizId is injected into the Present payload', async () => {
1711
+ mockMessageChannel();
1712
+ const { lb, iframe } = await renderLiveboard({ vizId });
1713
+ await executeAfterWait(() => {
1714
+ lb.trigger(HostEvent.Present, {} as any);
1715
+ expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(
1716
+ expect.objectContaining({
1717
+ data: expect.objectContaining({ vizId }),
1718
+ }),
1719
+ thoughtSpotHost,
1720
+ expect.anything(),
1721
+ );
1722
+ });
1723
+ });
1724
+ });
1725
+
1726
+ // ---------------------------------------------------------------------------
1727
+ // DownloadAsCsv — additional cases
1728
+ // ---------------------------------------------------------------------------
1729
+ describe('HostEvent.DownloadAsCsv — additional cases', () => {
1730
+ test('viewConfig.vizId overwrites an explicit payload vizId', async () => {
1731
+ mockMessageChannel();
1732
+ const { lb, iframe } = await renderLiveboard({ vizId });
1733
+ await executeAfterWait(() => {
1734
+ lb.trigger(HostEvent.DownloadAsCsv, { vizId: 'explicit-id' } as any);
1735
+ const call = (iframe.contentWindow.postMessage as jest.Mock).mock.calls[0];
1736
+ const sentData = call[0].data;
1737
+ expect(sentData.vizId).toBe(vizId); // viewConfig.vizId wins
1738
+ });
1739
+ });
1740
+
1741
+ test('returns null and calls handleError when !isRendered', async () => {
1742
+ const lb = unrenderedLiveboard();
1743
+ const result = await lb.trigger(HostEvent.DownloadAsCsv, {} as any);
1744
+ expect(result).toBeNull();
1745
+ expect((lb as any).handleError).toHaveBeenCalledWith(
1746
+ expect.objectContaining({ code: EmbedErrorCodes.RENDER_NOT_CALLED }),
1747
+ );
1748
+ });
1749
+
1750
+ test('returns null and logs debug when !iFrame', async () => {
1751
+ const lb = new LiveboardEmbed(getRootEl(), {
1752
+ frameParams: { width: '100%', height: '100%' },
1753
+ liveboardId,
1754
+ });
1755
+ (lb as any).isRendered = true;
1756
+ const result = await lb.trigger(HostEvent.DownloadAsCsv, {} as any);
1757
+ expect(result).toBeNull();
1758
+ });
1759
+ });
@@ -7,6 +7,43 @@ export interface LiveboardTab {
7
7
  [key: string]: any;
8
8
  }
9
9
 
10
+ /**
11
+ * Levels at which a filter or parameter can be applied.
12
+ */
13
+ export enum ApplicabilityLevel {
14
+ Liveboard = 'LIVEBOARD',
15
+ Tab = 'TAB',
16
+ Group = 'GROUP',
17
+ }
18
+
19
+ /**
20
+ * Scopes a filter or parameter to a specific target.
21
+ * At `LIVEBOARD` level the filter applies to the whole Liveboard, so `targetId`
22
+ * is not required.
23
+ */
24
+ export interface Applicability {
25
+ level: ApplicabilityLevel;
26
+ targetId?: string;
27
+ }
28
+
29
+ export interface FilterUpdate {
30
+ column: string;
31
+ oper: string;
32
+ values: string[];
33
+ type?: string;
34
+ applicability?: Applicability;
35
+ }
36
+
37
+ export interface LiveboardFilter {
38
+ applicability?: Applicability;
39
+ [key: string]: any;
40
+ }
41
+
42
+ export interface LiveboardParameter {
43
+ applicability?: Applicability;
44
+ [key: string]: any;
45
+ }
46
+
10
47
  export enum UIPassthroughEvent {
11
48
  PinAnswerToLiveboard = 'addVizToPinboard',
12
49
  SaveAnswer = 'saveAnswer',
@@ -103,7 +140,7 @@ export type UIPassthroughContractBase = {
103
140
  vizId?: string;
104
141
  };
105
142
  response: {
106
- liveboardFilters: Record<string, any>[];
143
+ liveboardFilters: LiveboardFilter[];
107
144
  runtimeFilters: RuntimeFilter[];
108
145
  };
109
146
  };
@@ -116,7 +153,7 @@ export type UIPassthroughContractBase = {
116
153
  [UIPassthroughEvent.GetParameters]: {
117
154
  request: Record<string, never>;
118
155
  response: {
119
- parameters: Record<string, any>[];
156
+ parameters: LiveboardParameter[];
120
157
  };
121
158
  };
122
159
  [UIPassthroughEvent.GetTML]: {
@@ -143,18 +180,8 @@ export type UIPassthroughContractBase = {
143
180
  };
144
181
  [UIPassthroughEvent.UpdateFilters]: {
145
182
  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
- }[];
183
+ filter?: FilterUpdate;
184
+ filters?: FilterUpdate[];
158
185
  };
159
186
  response: unknown;
160
187
  };
@@ -311,6 +311,70 @@ 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
+
314
378
  it('should throw when DrillDown payload has no valid points', async () => {
315
379
  const { client } = createHostEventClient();
316
380
  const invalidPayload = {} as any;
@@ -816,4 +880,266 @@ describe('HostEventClient', () => {
816
880
  expect(result).toEqual({ ok: true });
817
881
  });
818
882
  });
883
+
884
+ describe('availablePassthroughKeysCache reuse', () => {
885
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
886
+
887
+ it('GetFilters: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
888
+ const { client } = createHostEventClient();
889
+ const filterResponse = [{ value: { liveboardFilters: [] as any[], runtimeFilters: [] as any[] } }];
890
+ mockProcessTrigger
891
+ .mockResolvedValueOnce(allKeys())
892
+ .mockResolvedValueOnce(filterResponse)
893
+ .mockResolvedValueOnce(filterResponse);
894
+
895
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
896
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
897
+
898
+ // 3 calls total: 1 GetAvailableUIPassthroughs + 2 GetFilters
899
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
900
+ expect(mockProcessTrigger).not.toHaveBeenNthCalledWith(
901
+ 3,
902
+ expect.anything(),
903
+ HostEvent.UIPassthrough,
904
+ mockThoughtSpotHost,
905
+ expect.objectContaining({ type: UIPassthroughEvent.GetAvailableUIPassthroughs }),
906
+ undefined,
907
+ );
908
+ });
909
+
910
+ it('GetTabs: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
911
+ const { client } = createHostEventClient();
912
+ const tabResponse = [{ value: { orderedTabIds: [] as any[], numberOfTabs: 0, Tabs: [] as any[] } }];
913
+ mockProcessTrigger
914
+ .mockResolvedValueOnce(allKeys())
915
+ .mockResolvedValueOnce(tabResponse)
916
+ .mockResolvedValueOnce(tabResponse);
917
+
918
+ await client.triggerHostEvent(HostEvent.GetTabs, {});
919
+ await client.triggerHostEvent(HostEvent.GetTabs, {});
920
+
921
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
922
+ });
923
+
924
+ it('GetParameters: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
925
+ const { client } = createHostEventClient();
926
+ const paramResponse = [{ value: { parameters: [] as any[] } }];
927
+ mockProcessTrigger
928
+ .mockResolvedValueOnce(allKeys())
929
+ .mockResolvedValueOnce(paramResponse)
930
+ .mockResolvedValueOnce(paramResponse);
931
+
932
+ await client.triggerHostEvent(HostEvent.GetParameters, {});
933
+ await client.triggerHostEvent(HostEvent.GetParameters, {});
934
+
935
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
936
+ });
937
+
938
+ it('GetAnswerSession: second call reuses cache — GetAvailableUIPassthroughs called only once', async () => {
939
+ const { client } = createHostEventClient();
940
+ const sessionResponse = [{ value: { session: 's1' } }];
941
+ mockProcessTrigger
942
+ .mockResolvedValueOnce(allKeys())
943
+ .mockResolvedValueOnce(sessionResponse)
944
+ .mockResolvedValueOnce(sessionResponse);
945
+
946
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, {});
947
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, {});
948
+
949
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
950
+ });
951
+
952
+ it('UIPassthrough direct trigger does not populate the cache', async () => {
953
+ const { client } = createHostEventClient();
954
+ mockProcessTrigger.mockResolvedValue([{ value: { result: 'ok' } }]);
955
+
956
+ await client.triggerHostEvent(HostEvent.UIPassthrough, {});
957
+ // Cache not populated — next GetFilters call must still fetch keys
958
+ mockProcessTrigger.mockResolvedValueOnce(allKeys());
959
+ mockProcessTrigger.mockResolvedValueOnce([{ value: { liveboardFilters: [], runtimeFilters: [] as any[] } }]);
960
+ await client.triggerHostEvent(HostEvent.GetFilters, {});
961
+
962
+ // UIPassthrough call + GetAvailableUIPassthroughs + GetFilters = 3
963
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(3);
964
+ });
965
+
966
+ it('GetAnswerSession: catch in getAvailableUIPassthroughKeys does not cache, so next call retries GetAvailableUIPassthroughs', async () => {
967
+ const { client } = createHostEventClient();
968
+ mockProcessTrigger.mockReset();
969
+
970
+ // Flow per triggerHostEvent call when GetAvailableUIPassthroughs rejects:
971
+ // 1. getAvailableUIPassthroughKeys → processTrigger(UIPassthrough, GetAvailableUIPassthroughs) → rejects → returns []
972
+ // 2. keys=[] so getDataWithPassthroughFallback is called →
973
+ // triggerUIPassthroughApi(GetAnswerSession) → processTrigger(UIPassthrough) → null → hostEventFallback called
974
+ // 3. hostEventFallback → processTrigger(GetAnswerSession) → legacy result
975
+
976
+ // First triggerHostEvent: 3 calls
977
+ mockProcessTrigger.mockRejectedValueOnce(new Error('network error')); // call 1
978
+ mockProcessTrigger.mockResolvedValueOnce(null); // call 2
979
+ mockProcessTrigger.mockResolvedValueOnce({ session: 'leg1' }); // call 3
980
+
981
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '1' });
982
+
983
+ // Second triggerHostEvent: cache was NOT set, so GetAvailableUIPassthroughs is retried: 3 more calls
984
+ mockProcessTrigger.mockRejectedValueOnce(new Error('network error')); // call 4
985
+ mockProcessTrigger.mockResolvedValueOnce(null); // call 5
986
+ mockProcessTrigger.mockResolvedValueOnce({ session: 'leg2' }); // call 6
987
+
988
+ await client.triggerHostEvent(HostEvent.GetAnswerSession, { vizId: '2' });
989
+
990
+ // 6 total calls: GetAvailableUIPassthroughs was called TWICE (cache not set after catch)
991
+ expect(mockProcessTrigger).toHaveBeenCalledTimes(6);
992
+ const availableCallsCount = mockProcessTrigger.mock.calls.filter(
993
+ (call) => call[3]?.type === UIPassthroughEvent.GetAvailableUIPassthroughs,
994
+ ).length;
995
+ expect(availableCallsCount).toBe(2);
996
+ });
997
+ });
998
+
999
+ describe('GetTML edge cases', () => {
1000
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
1001
+
1002
+ it('includeNonExecutedSearchTokens flag is forwarded in UIPassthrough parameters', async () => {
1003
+ const { client, mockIframe } = createHostEventClient();
1004
+ mockProcessTrigger
1005
+ .mockResolvedValueOnce(allKeys())
1006
+ .mockResolvedValueOnce([{ value: { tml: 'data' } }]);
1007
+
1008
+ await client.triggerHostEvent(HostEvent.GetTML, { includeNonExecutedSearchTokens: true } as any);
1009
+
1010
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1011
+ 2,
1012
+ mockIframe,
1013
+ HostEvent.UIPassthrough,
1014
+ mockThoughtSpotHost,
1015
+ {
1016
+ type: UIPassthroughEvent.GetTML,
1017
+ parameters: { includeNonExecutedSearchTokens: true },
1018
+ },
1019
+ undefined,
1020
+ );
1021
+ });
1022
+
1023
+ it('value.error (singular) throws — distinct from value.errors', async () => {
1024
+ const { client } = createHostEventClient();
1025
+ mockProcessTrigger
1026
+ .mockResolvedValueOnce(allKeys())
1027
+ .mockResolvedValueOnce([{ value: { error: 'TML fetch failed' } }]);
1028
+
1029
+ await expect(client.triggerHostEvent(HostEvent.GetTML, {}))
1030
+ .rejects.toThrow('TML fetch failed');
1031
+ });
1032
+
1033
+ it('GetTML key absent from non-empty keys array falls back to legacy hostEventFallback', async () => {
1034
+ const { client, mockIframe } = createHostEventClient();
1035
+ // Return keys that do NOT include GetTML
1036
+ mockProcessTrigger
1037
+ .mockResolvedValueOnce([{ value: { keys: [UIPassthroughEvent.GetFilters] } }])
1038
+ .mockResolvedValueOnce({ answer: { search_query: 'revenue' } });
1039
+
1040
+ const result = await client.triggerHostEvent(HostEvent.GetTML, {});
1041
+
1042
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1043
+ 2,
1044
+ mockIframe,
1045
+ HostEvent.GetTML,
1046
+ mockThoughtSpotHost,
1047
+ {},
1048
+ undefined,
1049
+ );
1050
+ expect(result).toEqual({ answer: { search_query: 'revenue' } });
1051
+ });
1052
+ });
1053
+
1054
+ describe('SaveAnswer key-presence edge cases', () => {
1055
+ const allKeys = () => [{ value: { keys: Object.values(UIPassthroughEvent) } }];
1056
+ const saveResponse = [{
1057
+ value: {
1058
+ saveResponse: {
1059
+ data: {
1060
+ Answer__save: {
1061
+ answer: { id: 'ans123' },
1062
+ },
1063
+ },
1064
+ },
1065
+ },
1066
+ refId: 'viz1',
1067
+ }];
1068
+
1069
+ it('name+description both empty string route through passthrough, not Save fallback', async () => {
1070
+ const { client, mockIframe } = createHostEventClient();
1071
+ mockProcessTrigger
1072
+ .mockResolvedValueOnce(allKeys())
1073
+ .mockResolvedValueOnce(saveResponse);
1074
+
1075
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { name: '', description: '' } as any);
1076
+
1077
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1078
+ 2,
1079
+ mockIframe,
1080
+ HostEvent.UIPassthrough,
1081
+ mockThoughtSpotHost,
1082
+ { type: UIPassthroughEvent.SaveAnswer, parameters: { name: '', description: '' } },
1083
+ undefined,
1084
+ );
1085
+ });
1086
+
1087
+ it('description: undefined (key present) routes through passthrough, not Save fallback', async () => {
1088
+ const { client, mockIframe } = createHostEventClient();
1089
+ mockProcessTrigger
1090
+ .mockResolvedValueOnce(allKeys())
1091
+ .mockResolvedValueOnce(saveResponse);
1092
+
1093
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { name: 'Test', description: undefined } as any);
1094
+
1095
+ expect(mockProcessTrigger).toHaveBeenNthCalledWith(
1096
+ 2,
1097
+ mockIframe,
1098
+ HostEvent.UIPassthrough,
1099
+ mockThoughtSpotHost,
1100
+ { type: UIPassthroughEvent.SaveAnswer, parameters: { name: 'Test', description: undefined } },
1101
+ undefined,
1102
+ );
1103
+ });
1104
+
1105
+ it('name key absent falls back to HostEvent.Save', async () => {
1106
+ const { client, mockIframe } = createHostEventClient();
1107
+ mockProcessTrigger
1108
+ .mockResolvedValueOnce(allKeys())
1109
+ .mockResolvedValueOnce([saveResponse[0].value]);
1110
+
1111
+ await client.triggerHostEvent(HostEvent.SaveAnswer, { description: 'desc only' } as any);
1112
+
1113
+ expect(mockProcessTrigger).toHaveBeenCalledWith(
1114
+ mockIframe,
1115
+ HostEvent.Save,
1116
+ mockThoughtSpotHost,
1117
+ { description: 'desc only' },
1118
+ undefined,
1119
+ );
1120
+ });
1121
+
1122
+ it('answerId is undefined when saveResponse.data.Answer__save.answer.id is missing', async () => {
1123
+ const { client } = createHostEventClient();
1124
+ const noIdSaveResponse = [{
1125
+ value: {
1126
+ saveResponse: {
1127
+ data: {
1128
+ Answer__save: {
1129
+ answer: {},
1130
+ },
1131
+ },
1132
+ },
1133
+ },
1134
+ refId: 'viz1',
1135
+ }];
1136
+ mockProcessTrigger
1137
+ .mockResolvedValueOnce(allKeys())
1138
+ .mockResolvedValueOnce(noIdSaveResponse);
1139
+
1140
+ const result = await client.triggerHostEvent(HostEvent.SaveAnswer, { name: 'Test', description: 'Desc' } as any);
1141
+
1142
+ expect(result.answerId).toBeUndefined();
1143
+ });
1144
+ });
819
1145
  });
@@ -3,8 +3,10 @@ import { processTrigger as processTriggerService } from '../../utils/processTrig
3
3
  import { getEmbedConfig } from '../embedConfig';
4
4
  import {
5
5
  isValidUpdateFiltersPayload,
6
+ isValidUpdateParametersPayload,
6
7
  isValidDrillDownPayload,
7
8
  throwUpdateFiltersValidationError,
9
+ throwUpdateParametersValidationError,
8
10
  throwDrillDownValidationError,
9
11
  } from './utils';
10
12
  import {
@@ -58,6 +60,7 @@ export class HostEventClient {
58
60
  [HostEvent.Pin]: (p, c) => this.handlePinEvent(p, c),
59
61
  [HostEvent.SaveAnswer]: (p, c) => this.handleSaveAnswerEvent(p, c),
60
62
  [HostEvent.UpdateFilters]: (p, c) => this.handleUpdateFiltersEvent(p, c),
63
+ [HostEvent.UpdateParameters]: (p, c) => this.handleUpdateParametersEvent(p, c),
61
64
  [HostEvent.DrillDown]: (p, c) => this.handleDrillDownEvent(p, c),
62
65
  };
63
66
  }
@@ -243,6 +246,18 @@ export class HostEventClient {
243
246
  return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, payload, context as ContextType);
244
247
  }
245
248
 
249
+ protected handleUpdateParametersEvent(
250
+ payload: HostEventRequest<HostEvent.UpdateParameters>,
251
+ context?: ContextType,
252
+ ): Promise<any> {
253
+ if (!isValidUpdateParametersPayload(payload)) {
254
+ throwUpdateParametersValidationError();
255
+ }
256
+
257
+ // UpdateParameters has no UI passthrough contract; dispatch over the legacy channel
258
+ return this.hostEventFallback(HostEvent.UpdateParameters, payload, context);
259
+ }
260
+
246
261
  protected handleDrillDownEvent(
247
262
  payload: HostEventRequest<HostEvent.DrillDown>,
248
263
  context?: ContextType,