@thoughtspot/visual-embed-sdk 1.46.4 → 1.46.5

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 (255) hide show
  1. package/cjs/package.json +3 -3
  2. package/cjs/src/auth.d.ts +2 -1
  3. package/cjs/src/auth.d.ts.map +1 -1
  4. package/cjs/src/auth.js +2 -1
  5. package/cjs/src/auth.js.map +1 -1
  6. package/cjs/src/authToken.d.ts.map +1 -1
  7. package/cjs/src/authToken.js.map +1 -1
  8. package/cjs/src/css-variables.d.ts +87 -17
  9. package/cjs/src/css-variables.d.ts.map +1 -1
  10. package/cjs/src/embed/app.d.ts +41 -1
  11. package/cjs/src/embed/app.d.ts.map +1 -1
  12. package/cjs/src/embed/app.js +25 -36
  13. package/cjs/src/embed/app.js.map +1 -1
  14. package/cjs/src/embed/app.spec.js +35 -23
  15. package/cjs/src/embed/app.spec.js.map +1 -1
  16. package/cjs/src/embed/base.d.ts.map +1 -1
  17. package/cjs/src/embed/base.js.map +1 -1
  18. package/cjs/src/embed/base.spec.js.map +1 -1
  19. package/cjs/src/embed/conversation.d.ts +23 -1
  20. package/cjs/src/embed/conversation.d.ts.map +1 -1
  21. package/cjs/src/embed/conversation.js +18 -33
  22. package/cjs/src/embed/conversation.js.map +1 -1
  23. package/cjs/src/embed/conversation.spec.js +129 -97
  24. package/cjs/src/embed/conversation.spec.js.map +1 -1
  25. package/cjs/src/embed/events.spec.js +72 -0
  26. package/cjs/src/embed/events.spec.js.map +1 -1
  27. package/cjs/src/embed/hostEventClient/contracts.d.ts +105 -3
  28. package/cjs/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  29. package/cjs/src/embed/hostEventClient/contracts.js +9 -0
  30. package/cjs/src/embed/hostEventClient/contracts.js.map +1 -1
  31. package/cjs/src/embed/hostEventClient/host-event-client.d.ts +28 -0
  32. package/cjs/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  33. package/cjs/src/embed/hostEventClient/host-event-client.js +106 -9
  34. package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
  35. package/cjs/src/embed/hostEventClient/host-event-client.spec.js +327 -6
  36. package/cjs/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  37. package/cjs/src/embed/hostEventClient/utils.d.ts +22 -0
  38. package/cjs/src/embed/hostEventClient/utils.d.ts.map +1 -0
  39. package/cjs/src/embed/hostEventClient/utils.js +51 -0
  40. package/cjs/src/embed/hostEventClient/utils.js.map +1 -0
  41. package/cjs/src/embed/hostEventClient/utils.spec.d.ts +2 -0
  42. package/cjs/src/embed/hostEventClient/utils.spec.d.ts.map +1 -0
  43. package/cjs/src/embed/hostEventClient/utils.spec.js +115 -0
  44. package/cjs/src/embed/hostEventClient/utils.spec.js.map +1 -0
  45. package/cjs/src/embed/liveboard.d.ts +18 -0
  46. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  47. package/cjs/src/embed/liveboard.js +10 -3
  48. package/cjs/src/embed/liveboard.js.map +1 -1
  49. package/cjs/src/embed/liveboard.spec.js +54 -2
  50. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  51. package/cjs/src/embed/sage.d.ts.map +1 -1
  52. package/cjs/src/embed/sage.js.map +1 -1
  53. package/cjs/src/embed/search.spec.js.map +1 -1
  54. package/cjs/src/embed/spotter-utils.d.ts +20 -0
  55. package/cjs/src/embed/spotter-utils.d.ts.map +1 -0
  56. package/cjs/src/embed/spotter-utils.js +52 -0
  57. package/cjs/src/embed/spotter-utils.js.map +1 -0
  58. package/cjs/src/embed/spotter-utils.spec.d.ts +2 -0
  59. package/cjs/src/embed/spotter-utils.spec.d.ts.map +1 -0
  60. package/cjs/src/embed/spotter-utils.spec.js +54 -0
  61. package/cjs/src/embed/spotter-utils.spec.js.map +1 -0
  62. package/cjs/src/embed/ts-embed.d.ts +41 -2
  63. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  64. package/cjs/src/embed/ts-embed.js +54 -3
  65. package/cjs/src/embed/ts-embed.js.map +1 -1
  66. package/cjs/src/errors.d.ts +2 -0
  67. package/cjs/src/errors.d.ts.map +1 -1
  68. package/cjs/src/errors.js +2 -0
  69. package/cjs/src/errors.js.map +1 -1
  70. package/cjs/src/react/index.d.ts.map +1 -1
  71. package/cjs/src/react/index.js +58 -53
  72. package/cjs/src/react/index.js.map +1 -1
  73. package/cjs/src/types.d.ts +832 -33
  74. package/cjs/src/types.d.ts.map +1 -1
  75. package/cjs/src/types.js +835 -3
  76. package/cjs/src/types.js.map +1 -1
  77. package/cjs/src/utils/graphql/answerService/answerService.d.ts +4 -2
  78. package/cjs/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  79. package/cjs/src/utils/graphql/answerService/answerService.js +4 -2
  80. package/cjs/src/utils/graphql/answerService/answerService.js.map +1 -1
  81. package/cjs/src/utils/graphql/preview-service.d.ts.map +1 -1
  82. package/cjs/src/utils/graphql/preview-service.js.map +1 -1
  83. package/cjs/src/utils/processData.d.ts.map +1 -1
  84. package/cjs/src/utils/processData.js.map +1 -1
  85. package/cjs/src/utils.d.ts +0 -9
  86. package/cjs/src/utils.d.ts.map +1 -1
  87. package/cjs/src/utils.js +1 -10
  88. package/cjs/src/utils.js.map +1 -1
  89. package/dist/index-ChNydfIz.js +7371 -0
  90. package/dist/index-DW2wEHqy.js +7371 -0
  91. package/dist/src/auth.d.ts +2 -1
  92. package/dist/src/auth.d.ts.map +1 -1
  93. package/dist/src/authToken.d.ts.map +1 -1
  94. package/dist/src/css-variables.d.ts +87 -17
  95. package/dist/src/css-variables.d.ts.map +1 -1
  96. package/dist/src/embed/app.d.ts +41 -1
  97. package/dist/src/embed/app.d.ts.map +1 -1
  98. package/dist/src/embed/base.d.ts.map +1 -1
  99. package/dist/src/embed/conversation.d.ts +23 -1
  100. package/dist/src/embed/conversation.d.ts.map +1 -1
  101. package/dist/src/embed/hostEventClient/contracts.d.ts +105 -3
  102. package/dist/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  103. package/dist/src/embed/hostEventClient/host-event-client.d.ts +28 -0
  104. package/dist/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  105. package/dist/src/embed/hostEventClient/utils.d.ts +22 -0
  106. package/dist/src/embed/hostEventClient/utils.d.ts.map +1 -0
  107. package/dist/src/embed/hostEventClient/utils.spec.d.ts +2 -0
  108. package/dist/src/embed/hostEventClient/utils.spec.d.ts.map +1 -0
  109. package/dist/src/embed/liveboard.d.ts +18 -0
  110. package/dist/src/embed/liveboard.d.ts.map +1 -1
  111. package/dist/src/embed/sage.d.ts.map +1 -1
  112. package/dist/src/embed/spotter-utils.d.ts +20 -0
  113. package/dist/src/embed/spotter-utils.d.ts.map +1 -0
  114. package/dist/src/embed/spotter-utils.spec.d.ts +2 -0
  115. package/dist/src/embed/spotter-utils.spec.d.ts.map +1 -0
  116. package/dist/src/embed/ts-embed.d.ts +41 -2
  117. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  118. package/dist/src/errors.d.ts +2 -0
  119. package/dist/src/errors.d.ts.map +1 -1
  120. package/dist/src/react/index.d.ts.map +1 -1
  121. package/dist/src/types.d.ts +832 -33
  122. package/dist/src/types.d.ts.map +1 -1
  123. package/dist/src/utils/graphql/answerService/answerService.d.ts +4 -2
  124. package/dist/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  125. package/dist/src/utils/graphql/preview-service.d.ts.map +1 -1
  126. package/dist/src/utils/processData.d.ts.map +1 -1
  127. package/dist/src/utils.d.ts +0 -9
  128. package/dist/src/utils.d.ts.map +1 -1
  129. package/dist/tsembed-react.es.js +1208 -154
  130. package/dist/tsembed-react.js +1206 -152
  131. package/dist/tsembed.es.js +1150 -101
  132. package/dist/tsembed.js +1148 -99
  133. package/dist/visual-embed-sdk-react-full.d.ts +1156 -55
  134. package/dist/visual-embed-sdk-react.d.ts +1156 -55
  135. package/dist/visual-embed-sdk.d.ts +1179 -60
  136. package/lib/package.json +3 -3
  137. package/lib/src/auth.d.ts +2 -1
  138. package/lib/src/auth.d.ts.map +1 -1
  139. package/lib/src/auth.js +2 -1
  140. package/lib/src/auth.js.map +1 -1
  141. package/lib/src/authToken.d.ts.map +1 -1
  142. package/lib/src/authToken.js.map +1 -1
  143. package/lib/src/css-variables.d.ts +87 -17
  144. package/lib/src/css-variables.d.ts.map +1 -1
  145. package/lib/src/embed/app.d.ts +41 -1
  146. package/lib/src/embed/app.d.ts.map +1 -1
  147. package/lib/src/embed/app.js +27 -38
  148. package/lib/src/embed/app.js.map +1 -1
  149. package/lib/src/embed/app.spec.js +35 -23
  150. package/lib/src/embed/app.spec.js.map +1 -1
  151. package/lib/src/embed/base.d.ts.map +1 -1
  152. package/lib/src/embed/base.js.map +1 -1
  153. package/lib/src/embed/base.spec.js.map +1 -1
  154. package/lib/src/embed/conversation.d.ts +23 -1
  155. package/lib/src/embed/conversation.d.ts.map +1 -1
  156. package/lib/src/embed/conversation.js +19 -34
  157. package/lib/src/embed/conversation.js.map +1 -1
  158. package/lib/src/embed/conversation.spec.js +131 -99
  159. package/lib/src/embed/conversation.spec.js.map +1 -1
  160. package/lib/src/embed/events.spec.js +73 -1
  161. package/lib/src/embed/events.spec.js.map +1 -1
  162. package/lib/src/embed/hostEventClient/contracts.d.ts +105 -3
  163. package/lib/src/embed/hostEventClient/contracts.d.ts.map +1 -1
  164. package/lib/src/embed/hostEventClient/contracts.js +9 -0
  165. package/lib/src/embed/hostEventClient/contracts.js.map +1 -1
  166. package/lib/src/embed/hostEventClient/host-event-client.d.ts +28 -0
  167. package/lib/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  168. package/lib/src/embed/hostEventClient/host-event-client.js +106 -9
  169. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  170. package/lib/src/embed/hostEventClient/host-event-client.spec.js +327 -6
  171. package/lib/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  172. package/lib/src/embed/hostEventClient/utils.d.ts +22 -0
  173. package/lib/src/embed/hostEventClient/utils.d.ts.map +1 -0
  174. package/lib/src/embed/hostEventClient/utils.js +43 -0
  175. package/lib/src/embed/hostEventClient/utils.js.map +1 -0
  176. package/lib/src/embed/hostEventClient/utils.spec.d.ts +2 -0
  177. package/lib/src/embed/hostEventClient/utils.spec.d.ts.map +1 -0
  178. package/lib/src/embed/hostEventClient/utils.spec.js +113 -0
  179. package/lib/src/embed/hostEventClient/utils.spec.js.map +1 -0
  180. package/lib/src/embed/liveboard.d.ts +18 -0
  181. package/lib/src/embed/liveboard.d.ts.map +1 -1
  182. package/lib/src/embed/liveboard.js +10 -3
  183. package/lib/src/embed/liveboard.js.map +1 -1
  184. package/lib/src/embed/liveboard.spec.js +54 -2
  185. package/lib/src/embed/liveboard.spec.js.map +1 -1
  186. package/lib/src/embed/sage.d.ts.map +1 -1
  187. package/lib/src/embed/sage.js.map +1 -1
  188. package/lib/src/embed/search.spec.js.map +1 -1
  189. package/lib/src/embed/spotter-utils.d.ts +20 -0
  190. package/lib/src/embed/spotter-utils.d.ts.map +1 -0
  191. package/lib/src/embed/spotter-utils.js +47 -0
  192. package/lib/src/embed/spotter-utils.js.map +1 -0
  193. package/lib/src/embed/spotter-utils.spec.d.ts +2 -0
  194. package/lib/src/embed/spotter-utils.spec.d.ts.map +1 -0
  195. package/lib/src/embed/spotter-utils.spec.js +52 -0
  196. package/lib/src/embed/spotter-utils.spec.js.map +1 -0
  197. package/lib/src/embed/ts-embed.d.ts +41 -2
  198. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  199. package/lib/src/embed/ts-embed.js +55 -4
  200. package/lib/src/embed/ts-embed.js.map +1 -1
  201. package/lib/src/errors.d.ts +2 -0
  202. package/lib/src/errors.d.ts.map +1 -1
  203. package/lib/src/errors.js +2 -0
  204. package/lib/src/errors.js.map +1 -1
  205. package/lib/src/react/index.d.ts.map +1 -1
  206. package/lib/src/react/index.js +58 -53
  207. package/lib/src/react/index.js.map +1 -1
  208. package/lib/src/types.d.ts +832 -33
  209. package/lib/src/types.d.ts.map +1 -1
  210. package/lib/src/types.js +835 -3
  211. package/lib/src/types.js.map +1 -1
  212. package/lib/src/utils/graphql/answerService/answerService.d.ts +4 -2
  213. package/lib/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  214. package/lib/src/utils/graphql/answerService/answerService.js +4 -2
  215. package/lib/src/utils/graphql/answerService/answerService.js.map +1 -1
  216. package/lib/src/utils/graphql/preview-service.d.ts.map +1 -1
  217. package/lib/src/utils/graphql/preview-service.js.map +1 -1
  218. package/lib/src/utils/processData.d.ts.map +1 -1
  219. package/lib/src/utils/processData.js.map +1 -1
  220. package/lib/src/utils.d.ts +0 -9
  221. package/lib/src/utils.d.ts.map +1 -1
  222. package/lib/src/utils.js +0 -8
  223. package/lib/src/utils.js.map +1 -1
  224. package/lib/src/visual-embed-sdk.d.ts +1179 -60
  225. package/package.json +3 -3
  226. package/src/auth.spec.ts +1 -1
  227. package/src/auth.ts +2 -1
  228. package/src/authToken.ts +0 -1
  229. package/src/css-variables.ts +96 -17
  230. package/src/embed/app.spec.ts +48 -30
  231. package/src/embed/app.ts +59 -54
  232. package/src/embed/base.spec.ts +1 -2
  233. package/src/embed/base.ts +1 -4
  234. package/src/embed/conversation.spec.ts +150 -119
  235. package/src/embed/conversation.ts +30 -54
  236. package/src/embed/events.spec.ts +88 -0
  237. package/src/embed/hostEventClient/contracts.ts +105 -2
  238. package/src/embed/hostEventClient/host-event-client.spec.ts +504 -6
  239. package/src/embed/hostEventClient/host-event-client.ts +146 -15
  240. package/src/embed/hostEventClient/utils.spec.ts +137 -0
  241. package/src/embed/hostEventClient/utils.ts +61 -0
  242. package/src/embed/liveboard.spec.ts +71 -2
  243. package/src/embed/liveboard.ts +29 -2
  244. package/src/embed/sage.ts +0 -1
  245. package/src/embed/search.spec.ts +0 -2
  246. package/src/embed/spotter-utils.spec.ts +56 -0
  247. package/src/embed/spotter-utils.ts +65 -0
  248. package/src/embed/ts-embed.ts +58 -4
  249. package/src/errors.ts +2 -0
  250. package/src/react/index.tsx +76 -72
  251. package/src/types.ts +843 -31
  252. package/src/utils/graphql/answerService/answerService.ts +4 -5
  253. package/src/utils/graphql/preview-service.ts +0 -1
  254. package/src/utils/processData.ts +0 -5
  255. package/src/utils.ts +0 -14
@@ -1,20 +1,65 @@
1
1
  import { ContextType, HostEvent } from '../../types';
2
2
  import { processTrigger as processTriggerService } from '../../utils/processTrigger';
3
3
  import { getEmbedConfig } from '../embedConfig';
4
+ import {
5
+ isValidUpdateFiltersPayload,
6
+ isValidDrillDownPayload,
7
+ throwUpdateFiltersValidationError,
8
+ throwDrillDownValidationError,
9
+ } from './utils';
4
10
  import {
5
11
  UIPassthroughArrayResponse,
6
- UIPassthroughEvent, HostEventRequest, HostEventResponse,
12
+ UIPassthroughEvent,
13
+ HostEventRequest,
14
+ HostEventResponse,
7
15
  UIPassthroughRequest,
8
16
  UIPassthroughResponse,
9
17
  TriggerPayload,
10
18
  TriggerResponse,
11
19
  } from './contracts';
12
20
 
21
+ /**
22
+ * Maps HostEvent to its corresponding UIPassthroughEvent.
23
+ * Includes both custom-handler events (Pin, SaveAnswer, UpdateFilters, DrillDown)
24
+ * and getter events (GetAnswerSession, GetFilters, etc.) that use getDataWithPassthroughFallback.
25
+ */
26
+ const PASSTHROUGH_MAP: Partial<Record<HostEvent, UIPassthroughEvent>> = {
27
+ // Custom handlers (setters with special logic)
28
+ [HostEvent.Pin]: UIPassthroughEvent.PinAnswerToLiveboard,
29
+ [HostEvent.SaveAnswer]: UIPassthroughEvent.SaveAnswer,
30
+ [HostEvent.UpdateFilters]: UIPassthroughEvent.UpdateFilters,
31
+ [HostEvent.DrillDown]: UIPassthroughEvent.Drilldown,
32
+ // Getters (use getDataWithPassthroughFallback)
33
+ [HostEvent.GetAnswerSession]: UIPassthroughEvent.GetAnswerSession,
34
+ [HostEvent.GetFilters]: UIPassthroughEvent.GetFilters,
35
+ [HostEvent.GetIframeUrl]: UIPassthroughEvent.GetIframeUrl,
36
+ [HostEvent.GetParameters]: UIPassthroughEvent.GetParameters,
37
+ [HostEvent.GetTML]: UIPassthroughEvent.GetTML,
38
+ [HostEvent.GetTabs]: UIPassthroughEvent.GetTabs,
39
+ [HostEvent.getExportRequestForCurrentPinboard]: UIPassthroughEvent.GetExportRequestForCurrentPinboard,
40
+ };
41
+
13
42
  export class HostEventClient {
14
43
  iFrame: HTMLIFrameElement;
15
44
 
45
+ /** Cached list of available UI passthrough keys from the embedded app */
46
+ private availablePassthroughKeysCache: string[] | null = null;
47
+
48
+ /** Host events with custom handlers
49
+ * (setters or special logic) -
50
+ * bound to instance for protected method access */
51
+ private readonly customHandlers: Partial<
52
+ Record<HostEvent, (payload: any, context?: ContextType) => Promise<any>>
53
+ >;
54
+
16
55
  constructor(iFrame?: HTMLIFrameElement) {
17
56
  this.iFrame = iFrame;
57
+ this.customHandlers = {
58
+ [HostEvent.Pin]: (p, c) => this.handlePinEvent(p, c),
59
+ [HostEvent.SaveAnswer]: (p, c) => this.handleSaveAnswerEvent(p, c),
60
+ [HostEvent.UpdateFilters]: (p, c) => this.handleUpdateFiltersEvent(p, c),
61
+ [HostEvent.DrillDown]: (p, c) => this.handleDrillDownEvent(p, c),
62
+ };
18
63
  }
19
64
 
20
65
  /**
@@ -44,11 +89,10 @@ export class HostEventClient {
44
89
  context?: ContextType,
45
90
  ): Promise<UIPassthroughResponse<UIPassthroughEventT>> {
46
91
  const response = (await this.triggerUIPassthroughApi(apiName, parameters, context))
47
- ?.filter?.((r) => r.error || r.value)[0];
92
+ ?.find?.((r) => r.error || r.value);
48
93
 
49
94
  if (!response) {
50
95
  const error = `No answer found${parameters.vizId ? ` for vizId: ${parameters.vizId}` : ''}.`;
51
-
52
96
  throw { error };
53
97
  }
54
98
 
@@ -57,8 +101,8 @@ export class HostEventClient {
57
101
  || (response.value as any)?.error;
58
102
 
59
103
  if (errors) {
60
-
61
- throw { error: response.error };
104
+ const message = typeof errors === 'string' ? errors : JSON.stringify(errors);
105
+ throw { error: message };
62
106
  }
63
107
 
64
108
  return { ...response.value };
@@ -72,6 +116,36 @@ export class HostEventClient {
72
116
  return this.processTrigger(hostEvent, data, context);
73
117
  }
74
118
 
119
+ /**
120
+ * For getter events that return data. Tries UI passthrough first;
121
+ * if the app doesn't support it (no response data), falls back to
122
+ * the legacy host event channel. Real errors are thrown as-is.
123
+ */
124
+ private async getDataWithPassthroughFallback<UIPassthroughEventT extends UIPassthroughEvent>(
125
+ passthroughEvent: UIPassthroughEventT,
126
+ hostEvent: HostEvent,
127
+ payload: any,
128
+ context?: ContextType,
129
+ ): Promise<UIPassthroughResponse<UIPassthroughEventT>> {
130
+ const response = await this.triggerUIPassthroughApi(
131
+ passthroughEvent, payload || {}, context,
132
+ );
133
+ const matched = response?.find?.((r) => r.error || r.value);
134
+ if (!matched) {
135
+ return this.hostEventFallback(hostEvent, payload, context);
136
+ }
137
+
138
+ const errors = matched.error
139
+ || (matched.value as any)?.errors
140
+ || (matched.value as any)?.error;
141
+ if (errors) {
142
+ const message = typeof errors === 'string' ? errors : JSON.stringify(errors);
143
+ throw new Error(message);
144
+ }
145
+
146
+ return { ...matched.value };
147
+ }
148
+
75
149
  /**
76
150
  * Setter for the iframe element used for host events
77
151
  * @param {HTMLIFrameElement} iFrame - the iframe element to set
@@ -80,6 +154,27 @@ export class HostEventClient {
80
154
  this.iFrame = iFrame;
81
155
  }
82
156
 
157
+ /**
158
+ * Fetches the list of available UI passthrough keys from the embedded app.
159
+ * Result is cached for the session. Returns empty array on failure.
160
+ */
161
+ private async getAvailableUIPassthroughKeys(context?: ContextType): Promise<string[]> {
162
+ if (this.availablePassthroughKeysCache !== null) {
163
+ return this.availablePassthroughKeysCache;
164
+ }
165
+ try {
166
+ const response = await this.triggerUIPassthroughApi(
167
+ UIPassthroughEvent.GetAvailableUIPassthroughs, {}, context,
168
+ );
169
+ const matched = response?.find?.((r) => r.value && !r.error);
170
+ const keys = matched?.value?.keys;
171
+ this.availablePassthroughKeysCache = Array.isArray(keys) ? keys : [];
172
+ return this.availablePassthroughKeysCache;
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+
83
178
  public async triggerUIPassthroughApi<UIPassthroughEventT extends UIPassthroughEvent>(
84
179
  apiName: UIPassthroughEventT,
85
180
  parameters: UIPassthroughRequest<UIPassthroughEventT>,
@@ -137,6 +232,37 @@ export class HostEventClient {
137
232
  };
138
233
  }
139
234
 
235
+ protected handleUpdateFiltersEvent(
236
+ payload: HostEventRequest<HostEvent.UpdateFilters>,
237
+ context?: ContextType,
238
+ ): Promise<any> {
239
+ if (!isValidUpdateFiltersPayload(payload)) {
240
+ throwUpdateFiltersValidationError();
241
+ }
242
+
243
+ return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, payload, context as ContextType);
244
+ }
245
+
246
+ protected handleDrillDownEvent(
247
+ payload: HostEventRequest<HostEvent.DrillDown>,
248
+ context?: ContextType,
249
+ ): Promise<any> {
250
+ if (!isValidDrillDownPayload(payload)) {
251
+ throwDrillDownValidationError();
252
+ }
253
+
254
+ return this.handleHostEventWithParam(UIPassthroughEvent.Drilldown, payload, context as ContextType);
255
+ }
256
+
257
+ /**
258
+ * Dispatches a host event using the appropriate channel:
259
+ * 1. If the embedded app supports UI passthrough for this event, use it (custom handler or getter).
260
+ * 2. Otherwise fall back to the legacy host event channel.
261
+ *
262
+ * @param hostEvent - The host event to trigger
263
+ * @param payload - Optional payload for the event
264
+ * @param context - Optional context (e.g. vizId) for scoped operations
265
+ */
140
266
  public async triggerHostEvent<
141
267
  HostEventT extends HostEvent,
142
268
  PayloadT,
@@ -146,16 +272,21 @@ export class HostEventClient {
146
272
  payload?: TriggerPayload<PayloadT, HostEventT>,
147
273
  context?: ContextT,
148
274
  ): Promise<TriggerResponse<PayloadT, HostEventT, ContextType>> {
149
- switch (hostEvent) {
150
- case HostEvent.Pin:
151
- return this.handlePinEvent(payload as HostEventRequest<HostEvent.Pin>, context as ContextType) as any;
152
- case HostEvent.SaveAnswer:
153
- return this.handleSaveAnswerEvent(
154
- payload as HostEventRequest<HostEvent.SaveAnswer>,
155
- context as ContextType,
156
- ) as any;
157
- default:
158
- return this.hostEventFallback(hostEvent, payload, context);
275
+ const customHandler = this.customHandlers[hostEvent];
276
+ const passthroughEvent = PASSTHROUGH_MAP[hostEvent];
277
+
278
+ // If embedded app supports passthrough but not this event, use legacy channel
279
+ const keys = passthroughEvent ? await this.getAvailableUIPassthroughKeys(context as ContextType) : [];
280
+ if (passthroughEvent && keys.length > 0 && !keys.includes(passthroughEvent)) {
281
+ return this.hostEventFallback(hostEvent, payload, context) as any;
159
282
  }
283
+
284
+ // Custom handler (setters) > getter passthrough > legacy fallback
285
+ return (customHandler
286
+ ? customHandler(payload, context as ContextType)
287
+ : passthroughEvent
288
+ ? this.getDataWithPassthroughFallback(passthroughEvent, hostEvent, payload, context as ContextType)
289
+ : this.hostEventFallback(hostEvent, payload, context)
290
+ ) as any;
160
291
  }
161
292
  }
@@ -0,0 +1,137 @@
1
+ import {
2
+ isValidUpdateFiltersPayload,
3
+ isValidDrillDownPayload,
4
+ createValidationError,
5
+ throwUpdateFiltersValidationError,
6
+ throwDrillDownValidationError,
7
+ } from './utils';
8
+ import { ERROR_MESSAGE } from '../../errors';
9
+ import { EmbedEvent } from '../../types';
10
+ import { embedEventStatus } from '../../utils';
11
+
12
+ describe('hostEventClient utils', () => {
13
+ describe('isValidUpdateFiltersPayload', () => {
14
+ it('returns false for undefined', () => {
15
+ expect(isValidUpdateFiltersPayload(undefined)).toBe(false);
16
+ });
17
+
18
+ it('returns false for empty payload', () => {
19
+ expect(isValidUpdateFiltersPayload({})).toBe(false);
20
+ });
21
+
22
+ it('returns true for valid filter', () => {
23
+ expect(isValidUpdateFiltersPayload({
24
+ filter: { column: 'region', oper: 'EQ', values: ['North'] },
25
+ } as any)).toBe(true);
26
+ });
27
+
28
+ it('returns true for valid filters array', () => {
29
+ expect(isValidUpdateFiltersPayload({
30
+ filters: [
31
+ { column: 'x', oper: 'IN', values: ['a', 'b'] },
32
+ { column: 'y', oper: 'EQ', values: ['c'] },
33
+ ],
34
+ } as any)).toBe(true);
35
+ });
36
+
37
+ it('returns false for filter with missing column', () => {
38
+ expect(isValidUpdateFiltersPayload({
39
+ filter: { oper: 'EQ', values: ['a'] },
40
+ } as any)).toBe(false);
41
+ });
42
+
43
+ it('returns false for filter with missing oper', () => {
44
+ expect(isValidUpdateFiltersPayload({
45
+ filter: { column: 'x', values: ['a'] },
46
+ } as any)).toBe(false);
47
+ });
48
+
49
+ it('returns false for filter with non-array values', () => {
50
+ expect(isValidUpdateFiltersPayload({
51
+ filter: { column: 'x', oper: 'EQ', values: 'a' },
52
+ } as any)).toBe(false);
53
+ });
54
+
55
+ it('returns false for empty filters array', () => {
56
+ expect(isValidUpdateFiltersPayload({ filters: [] } as any)).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('isValidDrillDownPayload', () => {
61
+ it('returns false for undefined', () => {
62
+ expect(isValidDrillDownPayload(undefined)).toBe(false);
63
+ });
64
+
65
+ it('returns false for empty payload', () => {
66
+ expect(isValidDrillDownPayload({})).toBe(false);
67
+ });
68
+
69
+ it('returns false for empty points', () => {
70
+ expect(isValidDrillDownPayload({ points: {} } as any)).toBe(false);
71
+ });
72
+
73
+ it('returns false for non-object points', () => {
74
+ expect(isValidDrillDownPayload({ points: 'invalid' } as any)).toBe(false);
75
+ });
76
+
77
+ it('returns true for clickedPoint', () => {
78
+ expect(isValidDrillDownPayload({
79
+ points: { clickedPoint: 'point-1' },
80
+ } as any)).toBe(true);
81
+ });
82
+
83
+ it('returns true for selectedPoints', () => {
84
+ expect(isValidDrillDownPayload({
85
+ points: { selectedPoints: ['p1', 'p2'] },
86
+ } as any)).toBe(true);
87
+ });
88
+
89
+ it('returns true for both clickedPoint and selectedPoints', () => {
90
+ expect(isValidDrillDownPayload({
91
+ points: { clickedPoint: 'p1', selectedPoints: ['p2'] },
92
+ } as any)).toBe(true);
93
+ });
94
+
95
+ it('returns false for selectedPoints empty array', () => {
96
+ expect(isValidDrillDownPayload({
97
+ points: { selectedPoints: [] },
98
+ } as any)).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe('createValidationError', () => {
103
+ it('throws with message and embedErrorDetails', () => {
104
+ expect(() => createValidationError('test error')).toThrow('test error');
105
+
106
+ try {
107
+ createValidationError('custom msg');
108
+ } catch (err: any) {
109
+ expect(err.isValidationError).toBe(true);
110
+ expect(err.embedErrorDetails).toMatchObject({
111
+ type: EmbedEvent.Error,
112
+ data: {
113
+ errorType: 'VALIDATION_ERROR',
114
+ message: 'custom msg',
115
+ code: 'HOST_EVENT_VALIDATION',
116
+ error: 'custom msg',
117
+ },
118
+ status: embedEventStatus.END,
119
+ });
120
+ }
121
+ });
122
+ });
123
+
124
+ describe('throwUpdateFiltersValidationError', () => {
125
+ it('throws with UPDATEFILTERS_INVALID_PAYLOAD message', () => {
126
+ expect(() => throwUpdateFiltersValidationError())
127
+ .toThrow(ERROR_MESSAGE.UPDATEFILTERS_INVALID_PAYLOAD);
128
+ });
129
+ });
130
+
131
+ describe('throwDrillDownValidationError', () => {
132
+ it('throws with DRILLDOWN_INVALID_PAYLOAD message', () => {
133
+ expect(() => throwDrillDownValidationError())
134
+ .toThrow(ERROR_MESSAGE.DRILLDOWN_INVALID_PAYLOAD);
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,61 @@
1
+ import { EmbedErrorCodes, EmbedEvent, ErrorDetailsTypes, HostEvent } from '../../types';
2
+ import { ERROR_MESSAGE } from '../../errors';
3
+ import { HostEventRequest } from './contracts';
4
+ import { embedEventStatus } from '../../utils';
5
+
6
+ export function isValidUpdateFiltersPayload(
7
+ payload: HostEventRequest<HostEvent.UpdateFilters> | undefined,
8
+ ): boolean {
9
+ if (!payload) return false;
10
+
11
+ const isValidFilter = (f: { column?: string; oper?: string; values?: unknown[] }) =>
12
+ !!f && typeof f.column === 'string' && typeof f.oper === 'string' && Array.isArray(f.values);
13
+
14
+ const hasValidFilter = payload.filter && isValidFilter(payload.filter);
15
+ const hasValidFilters = Array.isArray(payload.filters) && payload.filters.length > 0 && payload.filters.every(isValidFilter);
16
+
17
+ return !!(hasValidFilter || hasValidFilters);
18
+ }
19
+
20
+ export function isValidDrillDownPayload(
21
+ payload: HostEventRequest<HostEvent.DrillDown> | undefined,
22
+ ): boolean {
23
+ if (!payload) return false;
24
+
25
+ const points = payload.points;
26
+ if (!points || typeof points !== 'object') return false;
27
+
28
+ const hasClickedPoint = 'clickedPoint' in points && points.clickedPoint != null;
29
+ const hasSelectedPoints = Array.isArray(points.selectedPoints) && points.selectedPoints.length > 0;
30
+
31
+ return hasClickedPoint || hasSelectedPoints;
32
+ }
33
+
34
+ export type ValidationError = Error & {
35
+ isValidationError?: boolean;
36
+ embedErrorDetails?: { type: EmbedEvent.Error; data: { errorType: ErrorDetailsTypes; message: string; code: EmbedErrorCodes; error: string }; status: typeof embedEventStatus.END };
37
+ };
38
+
39
+ export function createValidationError(message: string): never {
40
+ const err = new Error(message) as ValidationError;
41
+ err.isValidationError = true;
42
+ err.embedErrorDetails = {
43
+ type: EmbedEvent.Error,
44
+ data:{
45
+ errorType: ErrorDetailsTypes.VALIDATION_ERROR,
46
+ message,
47
+ code: EmbedErrorCodes.HOST_EVENT_VALIDATION,
48
+ error: message
49
+ },
50
+ status:embedEventStatus.END
51
+ };
52
+ throw err;
53
+ }
54
+
55
+ export function throwUpdateFiltersValidationError(): never {
56
+ createValidationError(ERROR_MESSAGE.UPDATEFILTERS_INVALID_PAYLOAD);
57
+ }
58
+
59
+ export function throwDrillDownValidationError(): never {
60
+ createValidationError(ERROR_MESSAGE.DRILLDOWN_INVALID_PAYLOAD);
61
+ }
@@ -94,6 +94,37 @@ describe('Liveboard/viz embed tests', () => {
94
94
  });
95
95
  });
96
96
 
97
+ test('should set disabled actions using PersonalizedViewsDropdown alias', async () => {
98
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
99
+ disabledActions: [Action.PersonalizedViewsDropdown],
100
+ disabledActionReason: 'Action denied',
101
+ ...defaultViewConfig,
102
+ liveboardId,
103
+ } as LiveboardViewConfig);
104
+ liveboardEmbed.render();
105
+ await executeAfterWait(() => {
106
+ expectUrlMatchesWithParams(
107
+ getIFrameSrc(),
108
+ `http://${thoughtSpotHost}/?embedApp=true&${defaultParamsWithoutHiddenActions}&disableAction=[%22${Action.PersonalisedViewsDropdown}%22]&disableHint=Action%20denied&hideAction=[%22${Action.ReportError}%22]${prefixParams}#/embed/viz/${liveboardId}`,
109
+ );
110
+ });
111
+ });
112
+
113
+ test('should set hidden actions using OrganizeFavorites alias', async () => {
114
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
115
+ hiddenActions: [Action.OrganizeFavorites],
116
+ ...defaultViewConfig,
117
+ liveboardId,
118
+ } as LiveboardViewConfig);
119
+ liveboardEmbed.render();
120
+ await executeAfterWait(() => {
121
+ expectUrlMatchesWithParams(
122
+ getIFrameSrc(),
123
+ `http://${thoughtSpotHost}/?embedApp=true&${defaultParamsWithoutHiddenActions}&hideAction=[%22${Action.ReportError}%22,%22${Action.OrganiseFavourites}%22]${prefixParams}#/embed/viz/${liveboardId}`,
124
+ );
125
+ });
126
+ });
127
+
97
128
  test('should set disabled actions', async () => {
98
129
  const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
99
130
  disabledActions: [Action.DownloadAsCsv, Action.DownloadAsPdf, Action.DownloadAsXlsx],
@@ -229,6 +260,21 @@ describe('Liveboard/viz embed tests', () => {
229
260
  });
230
261
  });
231
262
 
263
+ test('should set isWYSIWYGLiveboardPDFEnabled to true in url', async () => {
264
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
265
+ isContinuousLiveboardPDFEnabled: true,
266
+ ...defaultViewConfig,
267
+ liveboardId,
268
+ } as LiveboardViewConfig);
269
+ liveboardEmbed.render();
270
+ await executeAfterWait(() => {
271
+ expectUrlMatchesWithParams(
272
+ getIFrameSrc(),
273
+ `http://${thoughtSpotHost}/?embedApp=true${defaultParams}&isWYSIWYGLiveboardPDFEnabled=true${prefixParams}#/embed/viz/${liveboardId}`,
274
+ );
275
+ });
276
+ });
277
+
232
278
  test('should set isLiveboardXLSXCSVDownloadEnabled to true in url', async () => {
233
279
  const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
234
280
  isLiveboardXLSXCSVDownloadEnabled: true,
@@ -1011,8 +1057,8 @@ describe('Liveboard/viz embed tests', () => {
1011
1057
  } as LiveboardViewConfig);
1012
1058
  liveboardEmbed.render();
1013
1059
  await executeAfterWait(() => {
1014
- // URL: #/embed/viz/{id}/tab/{tabId}?view={viewId} (view at
1015
- // END, not middle)
1060
+ // URL: #/embed/viz/{id}/tab/{tabId}?view={viewId}
1061
+ // (view at END, not middle)
1016
1062
  expect(getIFrameSrc()).toMatch(
1017
1063
  new RegExp(
1018
1064
  `#/embed/viz/${liveboardId}/tab/${activeTabId}\\?view=${workaroundViewId}`,
@@ -1624,6 +1670,29 @@ describe('Liveboard/viz embed tests', () => {
1624
1670
  });
1625
1671
  });
1626
1672
 
1673
+ test('should send correct visible data when RequestVisibleEmbedCoordinates is triggered', async () => {
1674
+ const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
1675
+ ...defaultViewConfig,
1676
+ liveboardId,
1677
+ fullHeight: true,
1678
+ lazyLoadingMargin: '10px',
1679
+ } as LiveboardViewConfig);
1680
+
1681
+ const mockTrigger = jest.spyOn(liveboardEmbed, 'trigger');
1682
+
1683
+ await liveboardEmbed.render();
1684
+
1685
+ // Trigger the lazy load data calculation
1686
+ (liveboardEmbed as any).sendFullHeightLazyLoadData();
1687
+
1688
+ expect(mockTrigger).not.toHaveBeenCalledWith(HostEvent.VisibleEmbedCoordinates, {
1689
+ top: 0,
1690
+ height: 500,
1691
+ left: 0,
1692
+ width: 650,
1693
+ });
1694
+ });
1695
+
1627
1696
  test('should calculate correct visible data for partially visible full height element', async () => {
1628
1697
  mockIFrame.getBoundingClientRect = jest.fn().mockReturnValue({
1629
1698
  top: -50,
@@ -387,6 +387,24 @@ export interface LiveboardViewConfig extends BaseViewConfig, LiveboardOtherViewC
387
387
  * ```
388
388
  */
389
389
  isPNGInScheduledEmailsEnabled?: boolean;
390
+ /**
391
+ * Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
392
+ * following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
393
+ * This feature is GA from version 26.5.0.cl and is enabled by default on embed deployments.
394
+ *
395
+ * Supported embed types: `AppEmbed`, `LiveboardEmbed`
396
+ * @type {boolean}
397
+ * @version SDK: 1.48.0 | ThoughtSpot: 26.5.0.cl
398
+ * @example
399
+ * ```js
400
+ * // Replace <EmbedComponent> with embed component name. For example, AppEmbed or LiveboardEmbed
401
+ * const embed = new <EmbedComponent>('#tsEmbed', {
402
+ * ... // other embed view config
403
+ * isContinuousLiveboardPDFEnabled: true,
404
+ * })
405
+ * ```
406
+ */
407
+ isContinuousLiveboardPDFEnabled?: boolean;
390
408
  /**
391
409
  * This flag is used to enable/disable the XLSX/CSV download option for Liveboards
392
410
  *
@@ -598,6 +616,7 @@ export class LiveboardEmbed extends V1Embed {
598
616
  updatedSpotterChatPrompt,
599
617
  spotterChatConfig,
600
618
  isThisPeriodInDateFiltersEnabled,
619
+ isContinuousLiveboardPDFEnabled,
601
620
  } = this.viewConfig;
602
621
 
603
622
  const preventLiveboardFilterRemoval = this.viewConfig.preventLiveboardFilterRemoval
@@ -708,6 +727,10 @@ export class LiveboardEmbed extends V1Embed {
708
727
  params[Param.IsThisPeriodInDateFiltersEnabled] = isThisPeriodInDateFiltersEnabled;
709
728
  }
710
729
 
730
+ if (isContinuousLiveboardPDFEnabled !== undefined) {
731
+ params[Param.IsWYSIWYGLiveboardPDFEnabled] = isContinuousLiveboardPDFEnabled;
732
+ }
733
+
711
734
  params[Param.LiveboardHeaderSticky] = isLiveboardHeaderSticky;
712
735
  params[Param.LiveboardHeaderV2] = isLiveboardCompactHeaderEnabled;
713
736
  params[Param.ShowLiveboardVerifiedBadge] = showLiveboardVerifiedBadge;
@@ -732,7 +755,8 @@ export class LiveboardEmbed extends V1Embed {
732
755
  personalizedViewId?: string,
733
756
  ) {
734
757
  // Extract view from liveboardId if passed along with it (legacy
735
- // approach) View must be appended as query param at the end, not
758
+ // approach)
759
+ // View must be appended as query param at the end, not
736
760
  // embedded in path
737
761
  let liveboardGuid = liveboardId;
738
762
  let legacyViewId: string | undefined;
@@ -766,7 +790,10 @@ export class LiveboardEmbed extends V1Embed {
766
790
 
767
791
  private sendFullHeightLazyLoadData = () => {
768
792
  const data = calculateVisibleElementData(this.iFrame);
769
- this.trigger(HostEvent.VisibleEmbedCoordinates, data);
793
+ // this should be fired only if the lazyLoadingForFullHeight and fullHeight are true
794
+ if(this.viewConfig.lazyLoadingForFullHeight && this.viewConfig.fullHeight){
795
+ this.trigger(HostEvent.VisibleEmbedCoordinates, data);
796
+ }
770
797
  };
771
798
 
772
799
  /**
package/src/embed/sage.ts CHANGED
@@ -147,7 +147,6 @@ export class SageEmbed extends V1Embed {
147
147
  */
148
148
  protected viewConfig: SageViewConfig;
149
149
 
150
-
151
150
  constructor(domSelector: DOMSelector, viewConfig: SageViewConfig) {
152
151
  viewConfig.embedComponentType = 'SageEmbed';
153
152
  super(domSelector, viewConfig);
@@ -527,7 +527,6 @@ describe('Search embed tests', () => {
527
527
  test('should set dataPanelCustomGroupsAccordionInitialState to EXPAND_FIRST when passed', async () => {
528
528
  const searchEmbed = new SearchBarEmbed(getRootEl() as any, {
529
529
  ...defaultViewConfig,
530
-
531
530
  });
532
531
  searchEmbed.render();
533
532
  await executeAfterWait(() => {
@@ -541,7 +540,6 @@ describe('Search embed tests', () => {
541
540
  test('should set dataPanelCustomGroupsAccordionInitialState to EXPAND_FIRST when passed', async () => {
542
541
  const searchEmbed = new SearchEmbed(getRootEl(), {
543
542
  ...defaultViewConfig,
544
-
545
543
  dataPanelCustomGroupsAccordionInitialState:
546
544
  DataPanelCustomColumnGroupsAccordionState.EXPAND_FIRST,
547
545
  });
@@ -0,0 +1,56 @@
1
+ import { resolveEnablePastConversationsSidebar, buildSpotterSidebarAppInitData } from './spotter-utils';
2
+ import { ErrorDetailsTypes, EmbedErrorCodes } from '../types';
3
+ import { ERROR_MESSAGE } from '../errors';
4
+
5
+ describe('resolveEnablePastConversationsSidebar', () => {
6
+ it('prefers spotterSidebarConfig value over standalone', () => {
7
+ expect(resolveEnablePastConversationsSidebar({ spotterSidebarConfigValue: true, standaloneValue: false })).toBe(true);
8
+ expect(resolveEnablePastConversationsSidebar({ spotterSidebarConfigValue: false, standaloneValue: true })).toBe(false);
9
+ });
10
+
11
+ it('falls back to standalone when spotterSidebarConfig value is absent', () => {
12
+ expect(resolveEnablePastConversationsSidebar({ standaloneValue: true })).toBe(true);
13
+ });
14
+
15
+ it('returns undefined when both are absent', () => {
16
+ expect(resolveEnablePastConversationsSidebar({})).toBeUndefined();
17
+ });
18
+ });
19
+
20
+ describe('buildSpotterSidebarAppInitData', () => {
21
+ const base = { type: 'APP_INIT' } as any;
22
+ const noopError = jest.fn();
23
+
24
+ it('returns base unchanged when no sidebar config or standalone flag', () => {
25
+ const result = buildSpotterSidebarAppInitData(base, {}, noopError);
26
+ expect(result).toBe(base);
27
+ });
28
+
29
+ it('nests spotterSidebarConfig under embedParams', () => {
30
+ const result = buildSpotterSidebarAppInitData(base, {
31
+ spotterSidebarConfig: { enablePastConversationsSidebar: true, spotterSidebarTitle: 'Chats' },
32
+ }, noopError);
33
+ expect(result.embedParams?.spotterSidebarConfig).toEqual({
34
+ enablePastConversationsSidebar: true,
35
+ spotterSidebarTitle: 'Chats',
36
+ });
37
+ });
38
+
39
+ it('promotes standalone flag into spotterSidebarConfig.enablePastConversationsSidebar', () => {
40
+ const result = buildSpotterSidebarAppInitData(base, { enablePastConversationsSidebar: true }, noopError);
41
+ expect(result.embedParams?.spotterSidebarConfig?.enablePastConversationsSidebar).toBe(true);
42
+ });
43
+
44
+ it('calls handleError and strips spotterDocumentationUrl when invalid', () => {
45
+ const handleError = jest.fn();
46
+ const result = buildSpotterSidebarAppInitData(base, {
47
+ spotterSidebarConfig: { spotterDocumentationUrl: 'not-a-url' },
48
+ }, handleError);
49
+ expect(handleError).toHaveBeenCalledWith(expect.objectContaining({
50
+ errorType: ErrorDetailsTypes.VALIDATION_ERROR,
51
+ message: ERROR_MESSAGE.INVALID_SPOTTER_DOCUMENTATION_URL,
52
+ code: EmbedErrorCodes.INVALID_URL,
53
+ }));
54
+ expect(result.embedParams?.spotterSidebarConfig?.spotterDocumentationUrl).toBeUndefined();
55
+ });
56
+ });