@thoughtspot/visual-embed-sdk 1.47.3 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/package.json +1 -1
- package/cjs/src/auth.d.ts.map +1 -1
- package/cjs/src/auth.js +11 -1
- package/cjs/src/auth.js.map +1 -1
- package/cjs/src/auth.spec.js +38 -0
- package/cjs/src/auth.spec.js.map +1 -1
- package/cjs/src/authToken.d.ts +2 -0
- package/cjs/src/authToken.d.ts.map +1 -1
- package/cjs/src/authToken.js +7 -5
- package/cjs/src/authToken.js.map +1 -1
- package/cjs/src/css-variables.d.ts +140 -0
- package/cjs/src/css-variables.d.ts.map +1 -1
- package/cjs/src/embed/app.d.ts +63 -2
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js +57 -6
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/app.spec.js +200 -1
- package/cjs/src/embed/app.spec.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.js +7 -2
- package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.spec.js +385 -6
- package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/cjs/src/embed/base.d.ts +1 -0
- package/cjs/src/embed/base.d.ts.map +1 -1
- package/cjs/src/embed/base.js +13 -1
- package/cjs/src/embed/base.js.map +1 -1
- package/cjs/src/embed/base.spec.js +21 -0
- package/cjs/src/embed/base.spec.js.map +1 -1
- package/cjs/src/embed/bodyless-conversation.spec.js +86 -0
- package/cjs/src/embed/bodyless-conversation.spec.js.map +1 -1
- package/cjs/src/embed/conversation.d.ts +16 -1
- package/cjs/src/embed/conversation.d.ts.map +1 -1
- package/cjs/src/embed/conversation.js +5 -1
- package/cjs/src/embed/conversation.js.map +1 -1
- package/cjs/src/embed/conversation.spec.js +26 -0
- package/cjs/src/embed/conversation.spec.js.map +1 -1
- package/cjs/src/embed/liveboard.d.ts +48 -2
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +48 -7
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +139 -1
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/spotter-viz-utils.d.ts +85 -0
- package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -0
- package/cjs/src/embed/spotter-viz-utils.js +17 -0
- package/cjs/src/embed/spotter-viz-utils.js.map +1 -0
- package/cjs/src/embed/spotter-viz-utils.spec.d.ts +2 -0
- package/cjs/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
- package/cjs/src/embed/spotter-viz-utils.spec.js +31 -0
- package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -0
- package/cjs/src/embed/ts-embed.d.ts +58 -38
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +247 -151
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +397 -122
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +2 -1
- package/cjs/src/index.d.ts.map +1 -1
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/react/index.d.ts.map +1 -1
- package/cjs/src/react/index.js +3 -0
- package/cjs/src/react/index.js.map +1 -1
- package/cjs/src/tokenizedFetch.d.ts.map +1 -1
- package/cjs/src/tokenizedFetch.js +12 -9
- package/cjs/src/tokenizedFetch.js.map +1 -1
- package/cjs/src/tokenizedFetch.spec.d.ts +2 -0
- package/cjs/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/cjs/src/tokenizedFetch.spec.js +68 -0
- package/cjs/src/tokenizedFetch.spec.js.map +1 -0
- package/cjs/src/types.d.ts +309 -40
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +251 -23
- package/cjs/src/types.js.map +1 -1
- package/cjs/src/utils/authService/tokenizedAuthService.spec.js +6 -7
- package/cjs/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
- package/cjs/src/utils/logger.js +2 -1
- package/cjs/src/utils/logger.js.map +1 -1
- package/cjs/src/utils/logger.spec.d.ts +1 -0
- package/cjs/src/utils/logger.spec.d.ts.map +1 -1
- package/cjs/src/utils/logger.spec.js +10 -9
- package/cjs/src/utils/logger.spec.js.map +1 -1
- package/cjs/src/utils.d.ts +4 -1
- package/cjs/src/utils.d.ts.map +1 -1
- package/cjs/src/utils.js +107 -10
- package/cjs/src/utils.js.map +1 -1
- package/cjs/src/utils.spec.js +163 -4
- package/cjs/src/utils.spec.js.map +1 -1
- package/dist/{index-DZq20cR6.js → index-_UGCSSDR.js} +1 -1
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/authToken.d.ts +2 -0
- package/dist/src/authToken.d.ts.map +1 -1
- package/dist/src/css-variables.d.ts +140 -0
- package/dist/src/css-variables.d.ts.map +1 -1
- package/dist/src/embed/app.d.ts +63 -2
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/base.d.ts +1 -0
- package/dist/src/embed/base.d.ts.map +1 -1
- package/dist/src/embed/conversation.d.ts +16 -1
- package/dist/src/embed/conversation.d.ts.map +1 -1
- package/dist/src/embed/liveboard.d.ts +48 -2
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/spotter-viz-utils.d.ts +85 -0
- package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -0
- package/dist/src/embed/spotter-viz-utils.spec.d.ts +2 -0
- package/dist/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
- package/dist/src/embed/ts-embed.d.ts +58 -38
- package/dist/src/embed/ts-embed.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/react/index.d.ts.map +1 -1
- package/dist/src/tokenizedFetch.d.ts.map +1 -1
- package/dist/src/tokenizedFetch.spec.d.ts +2 -0
- package/dist/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/dist/src/types.d.ts +309 -40
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/logger.spec.d.ts +1 -0
- package/dist/src/utils/logger.spec.d.ts.map +1 -1
- package/dist/src/utils.d.ts +4 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/tsembed-react.es.js +3418 -2899
- package/dist/tsembed-react.js +3420 -2901
- package/dist/tsembed.es.js +3426 -2905
- package/dist/tsembed.js +3419 -2898
- package/dist/visual-embed-sdk-react-full.d.ts +687 -78
- package/dist/visual-embed-sdk-react.d.ts +687 -78
- package/dist/visual-embed-sdk.d.ts +702 -80
- package/lib/package.json +1 -1
- package/lib/src/auth.d.ts.map +1 -1
- package/lib/src/auth.js +12 -2
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +38 -0
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/authToken.d.ts +2 -0
- package/lib/src/authToken.d.ts.map +1 -1
- package/lib/src/authToken.js +2 -2
- package/lib/src/authToken.js.map +1 -1
- package/lib/src/css-variables.d.ts +140 -0
- package/lib/src/css-variables.d.ts.map +1 -1
- package/lib/src/embed/app.d.ts +63 -2
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js +58 -7
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/app.spec.js +201 -2
- package/lib/src/embed/app.spec.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.js +7 -2
- package/lib/src/embed/auto-frame-renderer.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.spec.js +387 -8
- package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/lib/src/embed/base.d.ts +1 -0
- package/lib/src/embed/base.d.ts.map +1 -1
- package/lib/src/embed/base.js +11 -0
- package/lib/src/embed/base.js.map +1 -1
- package/lib/src/embed/base.spec.js +22 -1
- package/lib/src/embed/base.spec.js.map +1 -1
- package/lib/src/embed/bodyless-conversation.spec.js +86 -0
- package/lib/src/embed/bodyless-conversation.spec.js.map +1 -1
- package/lib/src/embed/conversation.d.ts +16 -1
- package/lib/src/embed/conversation.d.ts.map +1 -1
- package/lib/src/embed/conversation.js +5 -1
- package/lib/src/embed/conversation.js.map +1 -1
- package/lib/src/embed/conversation.spec.js +27 -1
- package/lib/src/embed/conversation.spec.js.map +1 -1
- package/lib/src/embed/liveboard.d.ts +48 -2
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +49 -8
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +139 -1
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/spotter-viz-utils.d.ts +85 -0
- package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -0
- package/lib/src/embed/spotter-viz-utils.js +13 -0
- package/lib/src/embed/spotter-viz-utils.js.map +1 -0
- package/lib/src/embed/spotter-viz-utils.spec.d.ts +2 -0
- package/lib/src/embed/spotter-viz-utils.spec.d.ts.map +1 -0
- package/lib/src/embed/spotter-viz-utils.spec.js +29 -0
- package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -0
- package/lib/src/embed/ts-embed.d.ts +58 -38
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +250 -154
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +397 -122
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/react/index.d.ts.map +1 -1
- package/lib/src/react/index.js +3 -0
- package/lib/src/react/index.js.map +1 -1
- package/lib/src/tokenizedFetch.d.ts.map +1 -1
- package/lib/src/tokenizedFetch.js +13 -10
- package/lib/src/tokenizedFetch.js.map +1 -1
- package/lib/src/tokenizedFetch.spec.d.ts +2 -0
- package/lib/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/lib/src/tokenizedFetch.spec.js +65 -0
- package/lib/src/tokenizedFetch.spec.js.map +1 -0
- package/lib/src/types.d.ts +309 -40
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +251 -23
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/authService/tokenizedAuthService.spec.js +6 -7
- package/lib/src/utils/authService/tokenizedAuthService.spec.js.map +1 -1
- package/lib/src/utils/logger.js +2 -1
- package/lib/src/utils/logger.js.map +1 -1
- package/lib/src/utils/logger.spec.d.ts +1 -0
- package/lib/src/utils/logger.spec.d.ts.map +1 -1
- package/lib/src/utils/logger.spec.js +10 -9
- package/lib/src/utils/logger.spec.js.map +1 -1
- package/lib/src/utils.d.ts +4 -1
- package/lib/src/utils.d.ts.map +1 -1
- package/lib/src/utils.js +103 -9
- package/lib/src/utils.js.map +1 -1
- package/lib/src/utils.spec.js +164 -5
- package/lib/src/utils.spec.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +702 -80
- package/package.json +1 -1
- package/src/auth.spec.ts +55 -1
- package/src/auth.ts +11 -2
- package/src/authToken.ts +2 -2
- package/src/css-variables.ts +175 -1
- package/src/embed/app.spec.ts +260 -3
- package/src/embed/app.ts +127 -7
- package/src/embed/auto-frame-renderer.spec.ts +457 -58
- package/src/embed/auto-frame-renderer.ts +7 -2
- package/src/embed/base.spec.ts +25 -1
- package/src/embed/base.ts +19 -5
- package/src/embed/bodyless-conversation.spec.ts +93 -0
- package/src/embed/conversation.spec.ts +34 -0
- package/src/embed/conversation.ts +22 -1
- package/src/embed/liveboard.spec.ts +163 -1
- package/src/embed/liveboard.ts +106 -10
- package/src/embed/spotter-viz-utils.spec.ts +30 -0
- package/src/embed/spotter-viz-utils.ts +94 -0
- package/src/embed/ts-embed.spec.ts +564 -231
- package/src/embed/ts-embed.ts +384 -258
- package/src/index.ts +3 -0
- package/src/react/index.tsx +3 -0
- package/src/tokenizedFetch.spec.ts +81 -0
- package/src/tokenizedFetch.ts +14 -11
- package/src/types.ts +326 -36
- package/src/utils/authService/tokenizedAuthService.spec.ts +6 -6
- package/src/utils/logger.spec.ts +11 -9
- package/src/utils/logger.ts +2 -2
- package/src/utils.spec.ts +200 -4
- package/src/utils.ts +128 -9
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { startAutoMCPFrameRenderer } from './auto-frame-renderer';
|
|
2
|
-
import {
|
|
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}
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
});
|