@thoughtspot/visual-embed-sdk 1.48.0 → 1.49.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 (217) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/css-variables.d.ts +140 -0
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +62 -1
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js +57 -6
  7. package/cjs/src/embed/app.js.map +1 -1
  8. package/cjs/src/embed/app.spec.js +191 -1
  9. package/cjs/src/embed/app.spec.js.map +1 -1
  10. package/cjs/src/embed/auto-frame-renderer.js +7 -2
  11. package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
  12. package/cjs/src/embed/auto-frame-renderer.spec.js +385 -6
  13. package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
  14. package/cjs/src/embed/base.d.ts +1 -0
  15. package/cjs/src/embed/base.d.ts.map +1 -1
  16. package/cjs/src/embed/base.js +13 -1
  17. package/cjs/src/embed/base.js.map +1 -1
  18. package/cjs/src/embed/base.spec.js +21 -0
  19. package/cjs/src/embed/base.spec.js.map +1 -1
  20. package/cjs/src/embed/bodyless-conversation.spec.js +86 -0
  21. package/cjs/src/embed/bodyless-conversation.spec.js.map +1 -1
  22. package/cjs/src/embed/conversation.d.ts +16 -1
  23. package/cjs/src/embed/conversation.d.ts.map +1 -1
  24. package/cjs/src/embed/conversation.js +5 -1
  25. package/cjs/src/embed/conversation.js.map +1 -1
  26. package/cjs/src/embed/conversation.spec.js +26 -0
  27. package/cjs/src/embed/conversation.spec.js.map +1 -1
  28. package/cjs/src/embed/liveboard.d.ts +47 -1
  29. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  30. package/cjs/src/embed/liveboard.js +47 -6
  31. package/cjs/src/embed/liveboard.js.map +1 -1
  32. package/cjs/src/embed/liveboard.spec.js +129 -1
  33. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  34. package/cjs/src/embed/spotter-viz-utils.d.ts +85 -0
  35. package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -0
  36. package/cjs/src/embed/spotter-viz-utils.js +17 -0
  37. package/cjs/src/embed/spotter-viz-utils.js.map +1 -0
  38. package/cjs/src/embed/spotter-viz-utils.spec.d.ts +2 -0
  39. package/cjs/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
  40. package/cjs/src/embed/spotter-viz-utils.spec.js +31 -0
  41. package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -0
  42. package/cjs/src/embed/ts-embed.d.ts +58 -38
  43. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  44. package/cjs/src/embed/ts-embed.js +247 -151
  45. package/cjs/src/embed/ts-embed.js.map +1 -1
  46. package/cjs/src/embed/ts-embed.spec.js +369 -123
  47. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  48. package/cjs/src/index.d.ts +2 -1
  49. package/cjs/src/index.d.ts.map +1 -1
  50. package/cjs/src/index.js.map +1 -1
  51. package/cjs/src/mixpanel-service.js +2 -2
  52. package/cjs/src/mixpanel-service.js.map +1 -1
  53. package/cjs/src/react/index.d.ts.map +1 -1
  54. package/cjs/src/react/index.js +3 -0
  55. package/cjs/src/react/index.js.map +1 -1
  56. package/cjs/src/types.d.ts +267 -27
  57. package/cjs/src/types.d.ts.map +1 -1
  58. package/cjs/src/types.js +223 -19
  59. package/cjs/src/types.js.map +1 -1
  60. package/cjs/src/utils/authService/tokenizedAuthService.spec.js +6 -7
  61. package/cjs/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
  62. package/cjs/src/utils/logger.js +2 -1
  63. package/cjs/src/utils/logger.js.map +1 -1
  64. package/cjs/src/utils/logger.spec.d.ts +1 -0
  65. package/cjs/src/utils/logger.spec.d.ts.map +1 -1
  66. package/cjs/src/utils/logger.spec.js +10 -9
  67. package/cjs/src/utils/logger.spec.js.map +1 -1
  68. package/cjs/src/utils/sdk-version.d.ts +2 -0
  69. package/cjs/src/utils/sdk-version.d.ts.map +1 -0
  70. package/cjs/src/utils/sdk-version.js +7 -0
  71. package/cjs/src/utils/sdk-version.js.map +1 -0
  72. package/cjs/src/utils.d.ts +4 -1
  73. package/cjs/src/utils.d.ts.map +1 -1
  74. package/cjs/src/utils.js +107 -10
  75. package/cjs/src/utils.js.map +1 -1
  76. package/cjs/src/utils.spec.js +163 -4
  77. package/cjs/src/utils.spec.js.map +1 -1
  78. package/dist/{index-Ck-r09gt.js → index-B6Rn561t.js} +1 -1
  79. package/dist/src/css-variables.d.ts +140 -0
  80. package/dist/src/css-variables.d.ts.map +1 -1
  81. package/dist/src/embed/app.d.ts +62 -1
  82. package/dist/src/embed/app.d.ts.map +1 -1
  83. package/dist/src/embed/base.d.ts +1 -0
  84. package/dist/src/embed/base.d.ts.map +1 -1
  85. package/dist/src/embed/conversation.d.ts +16 -1
  86. package/dist/src/embed/conversation.d.ts.map +1 -1
  87. package/dist/src/embed/liveboard.d.ts +47 -1
  88. package/dist/src/embed/liveboard.d.ts.map +1 -1
  89. package/dist/src/embed/spotter-viz-utils.d.ts +85 -0
  90. package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -0
  91. package/dist/src/embed/spotter-viz-utils.spec.d.ts +2 -0
  92. package/dist/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
  93. package/dist/src/embed/ts-embed.d.ts +58 -38
  94. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  95. package/dist/src/index.d.ts +2 -1
  96. package/dist/src/index.d.ts.map +1 -1
  97. package/dist/src/react/index.d.ts.map +1 -1
  98. package/dist/src/types.d.ts +267 -27
  99. package/dist/src/types.d.ts.map +1 -1
  100. package/dist/src/utils/logger.spec.d.ts +1 -0
  101. package/dist/src/utils/logger.spec.d.ts.map +1 -1
  102. package/dist/src/utils/sdk-version.d.ts +2 -0
  103. package/dist/src/utils/sdk-version.d.ts.map +1 -0
  104. package/dist/src/utils.d.ts +4 -1
  105. package/dist/src/utils.d.ts.map +1 -1
  106. package/dist/tsembed-react.es.js +3710 -3226
  107. package/dist/tsembed-react.js +3360 -2876
  108. package/dist/tsembed.es.js +3715 -3229
  109. package/dist/tsembed.js +3710 -3224
  110. package/dist/visual-embed-sdk-react-full.d.ts +643 -63
  111. package/dist/visual-embed-sdk-react.d.ts +643 -63
  112. package/dist/visual-embed-sdk.d.ts +658 -65
  113. package/lib/package.json +1 -1
  114. package/lib/src/css-variables.d.ts +140 -0
  115. package/lib/src/css-variables.d.ts.map +1 -1
  116. package/lib/src/embed/app.d.ts +62 -1
  117. package/lib/src/embed/app.d.ts.map +1 -1
  118. package/lib/src/embed/app.js +58 -7
  119. package/lib/src/embed/app.js.map +1 -1
  120. package/lib/src/embed/app.spec.js +192 -2
  121. package/lib/src/embed/app.spec.js.map +1 -1
  122. package/lib/src/embed/auto-frame-renderer.js +7 -2
  123. package/lib/src/embed/auto-frame-renderer.js.map +1 -1
  124. package/lib/src/embed/auto-frame-renderer.spec.js +387 -8
  125. package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
  126. package/lib/src/embed/base.d.ts +1 -0
  127. package/lib/src/embed/base.d.ts.map +1 -1
  128. package/lib/src/embed/base.js +11 -0
  129. package/lib/src/embed/base.js.map +1 -1
  130. package/lib/src/embed/base.spec.js +22 -1
  131. package/lib/src/embed/base.spec.js.map +1 -1
  132. package/lib/src/embed/bodyless-conversation.spec.js +86 -0
  133. package/lib/src/embed/bodyless-conversation.spec.js.map +1 -1
  134. package/lib/src/embed/conversation.d.ts +16 -1
  135. package/lib/src/embed/conversation.d.ts.map +1 -1
  136. package/lib/src/embed/conversation.js +5 -1
  137. package/lib/src/embed/conversation.js.map +1 -1
  138. package/lib/src/embed/conversation.spec.js +27 -1
  139. package/lib/src/embed/conversation.spec.js.map +1 -1
  140. package/lib/src/embed/liveboard.d.ts +47 -1
  141. package/lib/src/embed/liveboard.d.ts.map +1 -1
  142. package/lib/src/embed/liveboard.js +48 -7
  143. package/lib/src/embed/liveboard.js.map +1 -1
  144. package/lib/src/embed/liveboard.spec.js +129 -1
  145. package/lib/src/embed/liveboard.spec.js.map +1 -1
  146. package/lib/src/embed/spotter-viz-utils.d.ts +85 -0
  147. package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -0
  148. package/lib/src/embed/spotter-viz-utils.js +13 -0
  149. package/lib/src/embed/spotter-viz-utils.js.map +1 -0
  150. package/lib/src/embed/spotter-viz-utils.spec.d.ts +2 -0
  151. package/lib/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
  152. package/lib/src/embed/spotter-viz-utils.spec.js +29 -0
  153. package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -0
  154. package/lib/src/embed/ts-embed.d.ts +58 -38
  155. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  156. package/lib/src/embed/ts-embed.js +249 -153
  157. package/lib/src/embed/ts-embed.js.map +1 -1
  158. package/lib/src/embed/ts-embed.spec.js +369 -123
  159. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  160. package/lib/src/index.d.ts +2 -1
  161. package/lib/src/index.d.ts.map +1 -1
  162. package/lib/src/index.js.map +1 -1
  163. package/lib/src/mixpanel-service.js +1 -1
  164. package/lib/src/mixpanel-service.js.map +1 -1
  165. package/lib/src/react/index.d.ts.map +1 -1
  166. package/lib/src/react/index.js +3 -0
  167. package/lib/src/react/index.js.map +1 -1
  168. package/lib/src/types.d.ts +267 -27
  169. package/lib/src/types.d.ts.map +1 -1
  170. package/lib/src/types.js +223 -19
  171. package/lib/src/types.js.map +1 -1
  172. package/lib/src/utils/authService/tokenizedAuthService.spec.js +6 -7
  173. package/lib/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
  174. package/lib/src/utils/logger.js +2 -1
  175. package/lib/src/utils/logger.js.map +1 -1
  176. package/lib/src/utils/logger.spec.d.ts +1 -0
  177. package/lib/src/utils/logger.spec.d.ts.map +1 -1
  178. package/lib/src/utils/logger.spec.js +10 -9
  179. package/lib/src/utils/logger.spec.js.map +1 -1
  180. package/lib/src/utils/sdk-version.d.ts +2 -0
  181. package/lib/src/utils/sdk-version.d.ts.map +1 -0
  182. package/lib/src/utils/sdk-version.js +3 -0
  183. package/lib/src/utils/sdk-version.js.map +1 -0
  184. package/lib/src/utils.d.ts +4 -1
  185. package/lib/src/utils.d.ts.map +1 -1
  186. package/lib/src/utils.js +103 -9
  187. package/lib/src/utils.js.map +1 -1
  188. package/lib/src/utils.spec.js +164 -5
  189. package/lib/src/utils.spec.js.map +1 -1
  190. package/lib/src/visual-embed-sdk.d.ts +658 -65
  191. package/package.json +1 -1
  192. package/src/css-variables.ts +175 -1
  193. package/src/embed/app.spec.ts +247 -3
  194. package/src/embed/app.ts +125 -5
  195. package/src/embed/auto-frame-renderer.spec.ts +457 -58
  196. package/src/embed/auto-frame-renderer.ts +7 -2
  197. package/src/embed/base.spec.ts +25 -1
  198. package/src/embed/base.ts +19 -5
  199. package/src/embed/bodyless-conversation.spec.ts +93 -0
  200. package/src/embed/conversation.spec.ts +34 -0
  201. package/src/embed/conversation.ts +22 -1
  202. package/src/embed/liveboard.spec.ts +149 -1
  203. package/src/embed/liveboard.ts +102 -6
  204. package/src/embed/spotter-viz-utils.spec.ts +30 -0
  205. package/src/embed/spotter-viz-utils.ts +94 -0
  206. package/src/embed/ts-embed.spec.ts +532 -234
  207. package/src/embed/ts-embed.ts +384 -258
  208. package/src/index.ts +3 -0
  209. package/src/mixpanel-service.ts +1 -1
  210. package/src/react/index.tsx +3 -0
  211. package/src/types.ts +284 -23
  212. package/src/utils/authService/tokenizedAuthService.spec.ts +6 -6
  213. package/src/utils/logger.spec.ts +11 -9
  214. package/src/utils/logger.ts +2 -2
  215. package/src/utils/sdk-version.ts +3 -0
  216. package/src/utils.spec.ts +200 -4
  217. package/src/utils.ts +128 -9
@@ -1,13 +1,17 @@
1
1
  import { startAutoMCPFrameRenderer } from './auto-frame-renderer';
2
- import { Param, AuthType } from '../types';
2
+ import { Action, AuthType, AutoMCPFrameRendererViewConfig, EmbedEvent, InterceptedApiType, Param } from '../types';
3
3
  import { init } from '../index';
4
4
  import * as authInstance from '../auth';
5
5
  import { TsEmbed } from './ts-embed';
6
+ import { LiveboardEmbed, LiveboardViewConfig } from './liveboard';
6
7
  import {
7
8
  getDocumentBody,
9
+ getRootEl,
10
+ postMessageToParent,
8
11
  } from '../test/test-utils';
9
12
 
10
13
  const thoughtSpotHost = 'tshost';
14
+ const TSMCP_SRC = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/viz/lb1`;
11
15
 
12
16
  describe('startAutoMCPFrameRenderer', () => {
13
17
  let renderIFrameSpy: jest.SpyInstance;
@@ -28,7 +32,7 @@ describe('startAutoMCPFrameRenderer', () => {
28
32
  TsEmbed.prototype as any,
29
33
  'getEmbedBasePath',
30
34
  ).mockImplementation(function (this: any, query: string) {
31
- return `http://${thoughtSpotHost}/?${query}#`;
35
+ return `http://${thoughtSpotHost}/${query}#`;
32
36
  });
33
37
  renderIFrameSpy = jest.spyOn(
34
38
  TsEmbed.prototype as any,
@@ -41,6 +45,59 @@ describe('startAutoMCPFrameRenderer', () => {
41
45
  getEmbedBasePathSpy.mockRestore();
42
46
  });
43
47
 
48
+ // ─── helpers ──────────────────────────────────────────────────────────────
49
+
50
+ /** Capture the src passed to renderIFrame for the first tsmcp iframe added */
51
+ async function captureRenderedSrc(viewConfig: AutoMCPFrameRendererViewConfig = {}): Promise<string> {
52
+ let capturedSrc = '';
53
+ renderIFrameSpy.mockRestore();
54
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
55
+ .mockImplementation(async function (this: any, src: string) {
56
+ capturedSrc = src;
57
+ });
58
+
59
+ const observer = startAutoMCPFrameRenderer(viewConfig);
60
+ const iframe = document.createElement('iframe');
61
+ iframe.src = TSMCP_SRC;
62
+ document.body.appendChild(iframe);
63
+ await new Promise((r) => setTimeout(r, 50));
64
+ observer.disconnect();
65
+ return capturedSrc;
66
+ }
67
+
68
+ /**
69
+ * Trigger a full APP_INIT cycle via the actual TsEmbed message infrastructure.
70
+ * Renders the replacement iframe into DOM so subscribeToMessageEvents fires,
71
+ * then fires APP_INIT and returns the port.postMessage response payload.
72
+ */
73
+ async function getAppInitResponse(viewConfig: AutoMCPFrameRendererViewConfig = {}): Promise<any> {
74
+ renderIFrameSpy.mockRestore();
75
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
76
+ .mockImplementation(async function (this: any, src: string) {
77
+ const iframe = document.createElement('iframe');
78
+ iframe.src = src;
79
+ this.setIframeElement(iframe);
80
+ this.handleInsertionIntoDOM(iframe);
81
+ this.subscribeToMessageEvents();
82
+ });
83
+
84
+ const observer = startAutoMCPFrameRenderer(viewConfig);
85
+ const tsmcpIframe = document.createElement('iframe');
86
+ tsmcpIframe.src = TSMCP_SRC;
87
+ document.body.appendChild(tsmcpIframe);
88
+ await new Promise((r) => setTimeout(r, 50));
89
+
90
+ const embeddedIframe = document.querySelector('iframe');
91
+ const mockPort: any = { postMessage: jest.fn() };
92
+ postMessageToParent(embeddedIframe.contentWindow, { type: EmbedEvent.APP_INIT, data: {} }, mockPort);
93
+ await new Promise((r) => setTimeout(r, 50));
94
+
95
+ observer.disconnect();
96
+ return mockPort.postMessage.mock.calls[0]?.[0];
97
+ }
98
+
99
+ // ─── MutationObserver setup ───────────────────────────────────────────────
100
+
44
101
  describe('MutationObserver setup', () => {
45
102
  test('should return a MutationObserver', () => {
46
103
  const observer = startAutoMCPFrameRenderer();
@@ -60,91 +117,74 @@ describe('startAutoMCPFrameRenderer', () => {
60
117
  });
61
118
  });
62
119
 
120
+ // ─── iframe detection via tsmcp param ─────────────────────────────────────
121
+
63
122
  describe('iframe detection via tsmcp param', () => {
64
123
  test('should process directly-added iframes with tsmcp=true', async () => {
65
124
  const observer = startAutoMCPFrameRenderer();
66
-
67
125
  const iframe = document.createElement('iframe');
68
126
  iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/viz/lb1/tab1`;
69
127
  document.body.appendChild(iframe);
70
-
71
128
  await new Promise((r) => setTimeout(r, 50));
72
-
73
129
  expect(renderIFrameSpy).toHaveBeenCalled();
74
130
  observer.disconnect();
75
131
  });
76
132
 
77
133
  test('should not process iframes without tsmcp param', async () => {
78
134
  const observer = startAutoMCPFrameRenderer();
79
-
80
135
  const iframe = document.createElement('iframe');
81
136
  iframe.src = `https://${thoughtSpotHost}/v2/?embedApp=true#/embed/viz/lb1`;
82
137
  document.body.appendChild(iframe);
83
-
84
138
  await new Promise((r) => setTimeout(r, 50));
85
-
86
139
  expect(renderIFrameSpy).not.toHaveBeenCalled();
87
140
  observer.disconnect();
88
141
  });
89
142
 
90
143
  test('should process tsmcp iframes nested inside added elements', async () => {
91
144
  const observer = startAutoMCPFrameRenderer();
92
-
93
145
  const wrapper = document.createElement('div');
94
146
  const iframe = document.createElement('iframe');
95
147
  iframe.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true`;
96
148
  wrapper.appendChild(iframe);
97
149
  document.body.appendChild(wrapper);
98
-
99
150
  await new Promise((r) => setTimeout(r, 50));
100
-
101
151
  expect(renderIFrameSpy).toHaveBeenCalled();
102
152
  observer.disconnect();
103
153
  });
104
154
 
105
155
  test('should not process nested iframes without tsmcp param', async () => {
106
156
  const observer = startAutoMCPFrameRenderer();
107
-
108
157
  const wrapper = document.createElement('div');
109
158
  const iframe = document.createElement('iframe');
110
159
  iframe.src = `https://${thoughtSpotHost}/?embedApp=true`;
111
160
  wrapper.appendChild(iframe);
112
161
  document.body.appendChild(wrapper);
113
-
114
162
  await new Promise((r) => setTimeout(r, 50));
115
-
116
163
  expect(renderIFrameSpy).not.toHaveBeenCalled();
117
164
  observer.disconnect();
118
165
  });
119
166
 
120
167
  test('should ignore non-iframe element nodes', async () => {
121
168
  const observer = startAutoMCPFrameRenderer();
122
-
123
169
  const div = document.createElement('div');
124
170
  div.textContent = `${Param.Tsmcp}=true`;
125
171
  document.body.appendChild(div);
126
-
127
172
  await new Promise((r) => setTimeout(r, 50));
128
-
129
173
  expect(renderIFrameSpy).not.toHaveBeenCalled();
130
174
  observer.disconnect();
131
175
  });
132
176
 
133
177
  test('should ignore text nodes', async () => {
134
178
  const observer = startAutoMCPFrameRenderer();
135
-
136
179
  const text = document.createTextNode('tsmcp=true');
137
180
  document.body.appendChild(text);
138
-
139
181
  await new Promise((r) => setTimeout(r, 50));
140
-
141
182
  expect(renderIFrameSpy).not.toHaveBeenCalled();
142
183
  observer.disconnect();
143
184
  });
144
185
 
145
186
  test('should process multiple tsmcp iframes in one mutation', async () => {
146
187
  const observer = startAutoMCPFrameRenderer();
147
-
148
188
  const wrapper = document.createElement('div');
149
189
  const iframe1 = document.createElement('iframe');
150
190
  iframe1.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true&id=1`;
@@ -153,114 +193,473 @@ describe('startAutoMCPFrameRenderer', () => {
153
193
  wrapper.appendChild(iframe1);
154
194
  wrapper.appendChild(iframe2);
155
195
  document.body.appendChild(wrapper);
156
-
157
196
  await new Promise((r) => setTimeout(r, 50));
158
-
159
197
  expect(renderIFrameSpy).toHaveBeenCalledTimes(2);
160
198
  observer.disconnect();
161
199
  });
162
200
 
163
201
  test('should ignore iframes with invalid src URLs', async () => {
164
202
  const observer = startAutoMCPFrameRenderer();
165
-
166
203
  const iframe = document.createElement('iframe');
167
204
  iframe.src = 'about:blank';
168
205
  document.body.appendChild(iframe);
169
-
170
206
  await new Promise((r) => setTimeout(r, 50));
171
-
172
207
  expect(renderIFrameSpy).not.toHaveBeenCalled();
173
208
  observer.disconnect();
174
209
  });
175
210
  });
176
211
 
212
+ // ─── handleInsertionIntoDOM override ──────────────────────────────────────
213
+
177
214
  describe('handleInsertionIntoDOM override', () => {
178
215
  test('should replace the original iframe when renderIFrame inserts DOM', async () => {
179
216
  renderIFrameSpy.mockRestore();
180
-
181
217
  const replaceSpy = jest.fn();
182
-
183
- renderIFrameSpy = jest.spyOn(
184
- TsEmbed.prototype as any,
185
- 'renderIFrame',
186
- ).mockImplementation(async function (this: any) {
187
- const newIframe = document.createElement('iframe');
188
- newIframe.src = 'https://replaced.example.com';
189
- this.frameToReplace.replaceWith = replaceSpy;
190
- this.handleInsertionIntoDOM(newIframe);
191
- });
218
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
219
+ .mockImplementation(async function (this: any) {
220
+ const newIframe = document.createElement('iframe');
221
+ newIframe.src = 'https://replaced.example.com';
222
+ this.frameToReplace.replaceWith = replaceSpy;
223
+ this.handleInsertionIntoDOM(newIframe);
224
+ });
192
225
 
193
226
  const observer = startAutoMCPFrameRenderer();
194
-
195
227
  const iframe = document.createElement('iframe');
196
228
  iframe.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true`;
197
229
  document.body.appendChild(iframe);
198
-
199
230
  await new Promise((r) => setTimeout(r, 50));
200
-
201
231
  expect(replaceSpy).toHaveBeenCalled();
202
232
  observer.disconnect();
203
233
  });
204
234
  });
205
235
 
206
- describe('viewConfig forwarding', () => {
236
+ // ─── URL params forwarding ────────────────────────────────────────────────
237
+
238
+ describe('URL params forwarding', () => {
207
239
  test('should accept empty viewConfig', () => {
208
240
  const observer = startAutoMCPFrameRenderer({});
209
241
  expect(observer).toBeInstanceOf(MutationObserver);
210
242
  observer.disconnect();
211
243
  });
212
244
 
213
- test('should accept no arguments (default empty config)', () => {
245
+ test('should accept no arguments', () => {
214
246
  const observer = startAutoMCPFrameRenderer();
215
247
  expect(observer).toBeInstanceOf(MutationObserver);
216
248
  observer.disconnect();
217
249
  });
250
+
251
+ test('disabledActions → disableAction in rendered src', async () => {
252
+ const src = await captureRenderedSrc({ disabledActions: [Action.Pin] });
253
+ expect(src).toContain('disableAction');
254
+ });
255
+
256
+ test('disabledActionReason → disableHint in rendered src', async () => {
257
+ const src = await captureRenderedSrc({
258
+ disabledActions: [Action.Pin],
259
+ disabledActionReason: 'Upgrade required',
260
+ });
261
+ expect(src).toContain('disableHint');
262
+ });
263
+
264
+ test('hiddenActions → hideAction in rendered src as JSON array', async () => {
265
+ const src = await captureRenderedSrc({ hiddenActions: [Action.Pin] });
266
+ expect(src).toContain(`hideAction=${JSON.stringify([Action.ReportError, Action.Pin])}`);
267
+ });
268
+
269
+ test('visibleActions → visibleAction in rendered src', async () => {
270
+ const src = await captureRenderedSrc({ visibleActions: [Action.Download] });
271
+ expect(src).toContain('visibleAction');
272
+ });
273
+
274
+ test('locale → locale param in rendered src', async () => {
275
+ const src = await captureRenderedSrc({ locale: 'fr-FR' });
276
+ expect(src).toContain('locale=fr-FR');
277
+ });
278
+
279
+ test('showAlerts → showAlerts param in rendered src', async () => {
280
+ const src = await captureRenderedSrc({ showAlerts: true });
281
+ expect(src).toContain('showAlerts=true');
282
+ });
283
+
284
+ test('exposeTranslationIDs → exposeTranslationIDs in rendered src', async () => {
285
+ const src = await captureRenderedSrc({ exposeTranslationIDs: true });
286
+ expect(src).toContain('exposeTranslationIDs=true');
287
+ });
288
+
289
+ test('disableRedirectionLinksInNewTab → param in rendered src', async () => {
290
+ const src = await captureRenderedSrc({ disableRedirectionLinksInNewTab: true });
291
+ expect(src).toContain('disableRedirectionLinksInNewTab=true');
292
+ });
293
+
294
+ test('overrideOrgId → orgId param in rendered src', async () => {
295
+ const src = await captureRenderedSrc({ overrideOrgId: 42 });
296
+ expect(src).toContain('orgId=42');
297
+ });
298
+
299
+ test('linkOverride (V1) auto-upgrades to V2 in rendered src', async () => {
300
+ const src = await captureRenderedSrc({ linkOverride: true });
301
+ expect(src).toContain('linkOverride=true');
302
+ expect(src).toContain('enableLinkOverridesV2=true');
303
+ });
304
+
305
+ test('enableLinkOverridesV2 → enableLinkOverridesV2 + linkOverride in rendered src', async () => {
306
+ const src = await captureRenderedSrc({ enableLinkOverridesV2: true });
307
+ expect(src).toContain('enableLinkOverridesV2=true');
308
+ expect(src).toContain('linkOverride=true');
309
+ });
310
+
311
+ test('disableRedirectionLinksInNewTab auto-disables V2 link overrides', async () => {
312
+ const src = await captureRenderedSrc({
313
+ enableLinkOverridesV2: true,
314
+ disableRedirectionLinksInNewTab: true,
315
+ });
316
+ expect(src).not.toContain('enableLinkOverridesV2=true');
317
+ expect(src).not.toContain('linkOverride=true');
318
+ expect(src).toContain('disableRedirectionLinksInNewTab=true');
319
+ });
320
+
321
+ test('disableRedirectionLinksInNewTab auto-disables V1 link override', async () => {
322
+ const src = await captureRenderedSrc({
323
+ linkOverride: true,
324
+ disableRedirectionLinksInNewTab: true,
325
+ });
326
+ expect(src).not.toContain('linkOverride=true');
327
+ expect(src).toContain('disableRedirectionLinksInNewTab=true');
328
+ });
329
+
330
+ test('additionalFlags → merged into rendered src', async () => {
331
+ const src = await captureRenderedSrc({ additionalFlags: { myFlag: 'hello', anotherFlag: 1 } });
332
+ expect(src).toContain('myFlag=hello');
333
+ expect(src).toContain('anotherFlag=1');
334
+ });
335
+
336
+ test('additionalFlags from viewConfig override those from init', async () => {
337
+ const src = await captureRenderedSrc({ additionalFlags: { overrideFlag: 'view' } });
338
+ expect(src).toContain('overrideFlag=view');
339
+ });
340
+
341
+ test('insertInToSlide → insertInToSlide param in rendered src', async () => {
342
+ const src = await captureRenderedSrc({ insertInToSlide: true });
343
+ expect(src).toContain('insertInToSlide=true');
344
+ });
345
+
346
+ test('customizations.iconSpriteUrl → iconSprite param in rendered src', async () => {
347
+ const src = await captureRenderedSrc({
348
+ customizations: { iconSpriteUrl: 'https://cdn.example.com/icons.svg' },
349
+ });
350
+ expect(src).toContain('iconSprite=cdn.example.com/icons.svg');
351
+ });
352
+
353
+ test('customizations.content.stringIDsUrl → overrideStringIDsUrl param in rendered src', async () => {
354
+ const src = await captureRenderedSrc({
355
+ customizations: { content: { stringIDsUrl: 'https://cdn.example.com/strings.json' } },
356
+ });
357
+ expect(src).toContain('overrideStringIDsUrl=');
358
+ });
359
+
360
+ test('multiple disabledActions → all actions serialised in rendered src', async () => {
361
+ const src = await captureRenderedSrc({
362
+ disabledActions: [Action.Pin, Action.Download, Action.Save],
363
+ });
364
+ expect(src).toContain('disableAction');
365
+ expect(src).toContain(Action.Pin);
366
+ expect(src).toContain(Action.Download);
367
+ expect(src).toContain(Action.Save);
368
+ });
369
+
370
+ test('multiple hiddenActions → all actions serialised in rendered src', async () => {
371
+ const src = await captureRenderedSrc({
372
+ hiddenActions: [Action.Pin, Action.Download],
373
+ });
374
+ const hideParam = decodeURIComponent(src.split('hideAction=')[1]?.split('&')[0] ?? '');
375
+ const parsed = JSON.parse(hideParam);
376
+ expect(parsed).toContain(Action.Pin);
377
+ expect(parsed).toContain(Action.Download);
378
+ });
379
+
380
+ test('multiple visibleActions → all actions serialised in rendered src', async () => {
381
+ const src = await captureRenderedSrc({
382
+ visibleActions: [Action.Download, Action.Save, Action.Pin],
383
+ });
384
+ const visibleParam = decodeURIComponent(src.split('visibleAction=')[1]?.split('&')[0] ?? '');
385
+ const parsed = JSON.parse(visibleParam);
386
+ expect(parsed).toContain(Action.Download);
387
+ expect(parsed).toContain(Action.Save);
388
+ expect(parsed).toContain(Action.Pin);
389
+ });
390
+
391
+ test('rendered src always contains a query string before the hash', async () => {
392
+ const src = await captureRenderedSrc();
393
+ expect(src).toMatch(/\?[^#]+#/);
394
+ });
395
+ });
396
+
397
+ // ─── frameParams forwarding ───────────────────────────────────────────────
398
+
399
+ describe('frameParams forwarding', () => {
400
+ test('frameParams.height and .width applied to the replacement iframe element', async () => {
401
+ renderIFrameSpy.mockRestore();
402
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
403
+ .mockResolvedValue(undefined);
404
+ const createIframeElSpy = jest.spyOn(TsEmbed.prototype as any, 'createIframeEl');
405
+
406
+ const observer = startAutoMCPFrameRenderer({ frameParams: { height: '600px', width: '100%' } });
407
+ const iframe = document.createElement('iframe');
408
+ iframe.src = TSMCP_SRC;
409
+ document.body.appendChild(iframe);
410
+ await new Promise((r) => setTimeout(r, 50));
411
+
412
+ if (createIframeElSpy.mock.calls.length > 0) {
413
+ const created = createIframeElSpy.mock.results[0]?.value as HTMLIFrameElement;
414
+ expect(created?.style?.height).toBe('600px');
415
+ expect(created?.style?.width).toBe('100%');
416
+ }
417
+ createIframeElSpy.mockRestore();
418
+ observer.disconnect();
419
+ });
420
+
421
+ test('frameParams custom HTML attributes applied to the iframe element', async () => {
422
+ renderIFrameSpy.mockRestore();
423
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
424
+ .mockResolvedValue(undefined);
425
+ const createIframeElSpy = jest.spyOn(TsEmbed.prototype as any, 'createIframeEl');
426
+
427
+ const observer = startAutoMCPFrameRenderer({
428
+ frameParams: { height: '400px', width: '800px', 'data-testid': 'my-embed' } as any,
429
+ });
430
+ const iframe = document.createElement('iframe');
431
+ iframe.src = TSMCP_SRC;
432
+ document.body.appendChild(iframe);
433
+ await new Promise((r) => setTimeout(r, 50));
434
+
435
+ if (createIframeElSpy.mock.calls.length > 0) {
436
+ const created = createIframeElSpy.mock.results[0]?.value as HTMLIFrameElement;
437
+ expect(created?.getAttribute('data-testid')).toBe('my-embed');
438
+ }
439
+ createIframeElSpy.mockRestore();
440
+ observer.disconnect();
441
+ });
218
442
  });
219
443
 
444
+ // ─── APP_INIT postMessage params forwarding ───────────────────────────────
445
+ //
446
+ // These params are not in the iframe src URL — they travel via the APP_INIT
447
+ // postMessage channel. AutoFrameRenderer inherits the TsEmbed APP_INIT
448
+ // handler, so the full round-trip is tested here.
449
+
450
+ describe('APP_INIT params forwarding', () => {
451
+ test('customizations.style.customCSS included in APP_INIT response', async () => {
452
+ const customizations = {
453
+ style: { customCSS: { variables: { '--ts-var-root-background': '#fff' } } },
454
+ };
455
+ const response = await getAppInitResponse({ customizations });
456
+ expect(response?.data?.customisations?.style?.customCSS).toEqual(
457
+ customizations.style.customCSS,
458
+ );
459
+ });
460
+
461
+ test('customizations.content.strings included in APP_INIT response', async () => {
462
+ const customizations = {
463
+ content: { strings: { DATA: 'Data' } },
464
+ };
465
+ const response = await getAppInitResponse({ customizations });
466
+ expect(response?.data?.customisations?.content?.strings?.DATA).toBe('Data');
467
+ });
468
+
469
+ test('customActions included in APP_INIT response', async () => {
470
+ const customActions = [
471
+ {
472
+ id: 'my-action',
473
+ name: 'My Action',
474
+ position: 'PRIMARY' as any,
475
+ target: 'ANSWER' as any,
476
+ },
477
+ ];
478
+ const response = await getAppInitResponse({ customActions });
479
+ expect(response?.data?.customActions).toEqual(
480
+ expect.arrayContaining([expect.objectContaining({ id: 'my-action' })]),
481
+ );
482
+ });
483
+
484
+ test('shouldBypassPayloadValidation forwarded via APP_INIT', async () => {
485
+ const response = await getAppInitResponse({ shouldBypassPayloadValidation: true });
486
+ expect(response?.data?.shouldBypassPayloadValidation).toBe(true);
487
+ });
488
+
489
+ test('useHostEventsV2 forwarded via APP_INIT', async () => {
490
+ const response = await getAppInitResponse({ useHostEventsV2: true });
491
+ expect(response?.data?.useHostEventsV2).toBe(true);
492
+ });
493
+
494
+ test('refreshAuthTokenOnNearExpiry forwarded as embedExpiryInAuthToken', async () => {
495
+ const response = await getAppInitResponse({ refreshAuthTokenOnNearExpiry: true });
496
+ expect(response?.data?.embedExpiryInAuthToken).toBe(true);
497
+ });
498
+
499
+ test('interceptUrls forwarded via APP_INIT', async () => {
500
+ // The SDK expands InterceptedApiType enum values into resolved prism endpoint URLs.
501
+ // Assert that the interceptUrls array is non-empty (i.e. the config was forwarded
502
+ // and processed) rather than checking the resolved strings directly.
503
+ const response = await getAppInitResponse({
504
+ interceptUrls: [InterceptedApiType.AnswerData],
505
+ });
506
+ expect(response?.data?.interceptUrls).toBeInstanceOf(Array);
507
+ expect(response?.data?.interceptUrls.length).toBeGreaterThan(0);
508
+ });
509
+
510
+ test('interceptTimeout forwarded via APP_INIT', async () => {
511
+ const response = await getAppInitResponse({ interceptTimeout: 5000 });
512
+ expect(response?.data?.interceptTimeout).toBe(5000);
513
+ });
514
+ });
515
+
516
+ // ─── getMCPIframeSrc URL construction ─────────────────────────────────────
517
+
220
518
  describe('getMCPIframeSrc URL construction', () => {
221
519
  test('should strip tsmcp param and merge embed params into rendered src', async () => {
222
520
  let capturedSrc = '';
223
521
  renderIFrameSpy.mockRestore();
224
- renderIFrameSpy = jest.spyOn(
225
- TsEmbed.prototype as any,
226
- 'renderIFrame',
227
- ).mockImplementation(async function (this: any, src: string) {
228
- capturedSrc = src;
229
- });
522
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
523
+ .mockImplementation(async function (this: any, src: string) {
524
+ capturedSrc = src;
525
+ });
230
526
 
231
527
  const observer = startAutoMCPFrameRenderer();
232
-
233
528
  const iframe = document.createElement('iframe');
234
529
  iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true&customParam=hello#/embed/viz`;
235
530
  document.body.appendChild(iframe);
236
-
237
531
  await new Promise((r) => setTimeout(r, 50));
238
532
 
239
533
  expect(capturedSrc).not.toContain(`${Param.Tsmcp}=true`);
240
534
  expect(capturedSrc).toContain('customParam=hello');
535
+ expect(capturedSrc).toMatch(/\?[^#]+#/);
241
536
  observer.disconnect();
242
537
  });
243
538
 
244
539
  test('should preserve hash from original iframe src', async () => {
245
540
  let capturedSrc = '';
246
541
  renderIFrameSpy.mockRestore();
247
- renderIFrameSpy = jest.spyOn(
248
- TsEmbed.prototype as any,
249
- 'renderIFrame',
250
- ).mockImplementation(async function (this: any, src: string) {
251
- capturedSrc = src;
252
- });
542
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
543
+ .mockImplementation(async function (this: any, src: string) {
544
+ capturedSrc = src;
545
+ });
253
546
 
254
547
  const observer = startAutoMCPFrameRenderer();
255
-
256
548
  const iframe = document.createElement('iframe');
257
549
  iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/viz/lb123`;
258
550
  document.body.appendChild(iframe);
259
-
260
551
  await new Promise((r) => setTimeout(r, 50));
261
552
 
262
553
  expect(capturedSrc).toContain('/embed/viz/lb123');
263
554
  observer.disconnect();
264
555
  });
556
+
557
+ test('should produce empty query string when no embed params and no source params', async () => {
558
+ let capturedSrc = '';
559
+ renderIFrameSpy.mockRestore();
560
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
561
+ .mockImplementation(async function (this: any, src: string) {
562
+ capturedSrc = src;
563
+ });
564
+
565
+ const observer = startAutoMCPFrameRenderer();
566
+ const iframe = document.createElement('iframe');
567
+ // Only tsmcp (stripped) — no other params
568
+ iframe.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true`;
569
+ document.body.appendChild(iframe);
570
+ await new Promise((r) => setTimeout(r, 50));
571
+
572
+ // At minimum the base embed params (hostAppUrl, sdkVersion, etc.) are always present
573
+ expect(capturedSrc).toMatch(/\?[^#]+#/);
574
+ observer.disconnect();
575
+ });
576
+
577
+ test('source params take precedence over viewConfig params for the same key', async () => {
578
+ let capturedSrc = '';
579
+ renderIFrameSpy.mockRestore();
580
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
581
+ .mockImplementation(async function (this: any, src: string) {
582
+ capturedSrc = src;
583
+ });
584
+
585
+ const observer = startAutoMCPFrameRenderer({ additionalFlags: { locale: 'en-US' } });
586
+ const iframe = document.createElement('iframe');
587
+ // Source overrides with de-DE
588
+ iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true&locale=de-DE#/embed/viz`;
589
+ document.body.appendChild(iframe);
590
+ await new Promise((r) => setTimeout(r, 50));
591
+
592
+ expect(capturedSrc).toContain('locale=de-DE');
593
+ observer.disconnect();
594
+ });
595
+ });
596
+
597
+ // ─── URL serialization parity with normal embeds ──────────────────────────
598
+ //
599
+ // These tests verify that array-typed params (hideAction, disableAction,
600
+ // visibleAction) are serialized identically by startAutoMCPFrameRenderer
601
+ // and by a standard LiveboardEmbed. Both must emit JSON arrays
602
+ // (e.g. ["pin"]) not CSV (e.g. pin) so ThoughtSpot's app honours them.
603
+
604
+ describe('URL serialization parity with LiveboardEmbed', () => {
605
+ async function captureLiveboardSrc(
606
+ viewConfig: Partial<LiveboardViewConfig>,
607
+ ): Promise<string> {
608
+ let capturedSrc = '';
609
+ renderIFrameSpy.mockRestore();
610
+ renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
611
+ .mockImplementation(async function (this: any, src: string) {
612
+ capturedSrc = src;
613
+ });
614
+ const embed = new LiveboardEmbed(getRootEl(), {
615
+ liveboardId: 'test-lb',
616
+ ...viewConfig,
617
+ });
618
+ embed.render();
619
+ await new Promise((r) => setTimeout(r, 50));
620
+ return capturedSrc;
621
+ }
622
+
623
+ function getQueryParam(url: string, param: string): string | null {
624
+ const qIdx = url.indexOf('?');
625
+ const hIdx = url.indexOf('#');
626
+ if (qIdx === -1) return null;
627
+ const qs = hIdx === -1 ? url.slice(qIdx + 1) : url.slice(qIdx + 1, hIdx);
628
+ return new URLSearchParams(qs).get(param);
629
+ }
630
+
631
+ test.each([
632
+ ['hiddenActions', { hiddenActions: [Action.Pin] }, 'hideAction'],
633
+ ['disabledActions', { disabledActions: [Action.Pin] }, 'disableAction'],
634
+ ['visibleActions', { visibleActions: [Action.Download] }, 'visibleAction'],
635
+ ])(
636
+ '%s: auto-renderer and LiveboardEmbed produce identical param format',
637
+ async (_, viewConfig, paramName) => {
638
+ const liveboardSrc = await captureLiveboardSrc(viewConfig);
639
+ const autoSrc = await captureRenderedSrc(viewConfig);
640
+
641
+ const liveboardValue = getQueryParam(liveboardSrc, paramName);
642
+ const autoValue = getQueryParam(autoSrc, paramName);
643
+
644
+ expect(autoValue).not.toBeNull();
645
+ expect(liveboardValue).not.toBeNull();
646
+ expect(autoValue).toBe(liveboardValue);
647
+ },
648
+ );
649
+
650
+ test('hideAction value is a JSON array, not CSV', async () => {
651
+ const autoSrc = await captureRenderedSrc({ hiddenActions: [Action.Pin] });
652
+ const liveboardSrc = await captureLiveboardSrc({ hiddenActions: [Action.Pin] });
653
+
654
+ const autoValue = getQueryParam(autoSrc, 'hideAction');
655
+ const liveboardValue = getQueryParam(liveboardSrc, 'hideAction');
656
+
657
+ // Both must parse as a JSON array — not CSV like "reportError,pin"
658
+ expect(() => JSON.parse(autoValue)).not.toThrow();
659
+ expect(() => JSON.parse(liveboardValue)).not.toThrow();
660
+ expect(Array.isArray(JSON.parse(autoValue))).toBe(true);
661
+ expect(Array.isArray(JSON.parse(liveboardValue))).toBe(true);
662
+ expect(JSON.parse(autoValue)).toEqual(JSON.parse(liveboardValue));
663
+ });
265
664
  });
266
665
  });