@thoughtspot/visual-embed-sdk 1.52.2-beta.2 → 1.53.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.
- package/cjs/package.json +1 -1
- package/cjs/src/contracts/contracts.spec.js +4 -0
- package/cjs/src/contracts/contracts.spec.js.map +1 -1
- package/cjs/src/contracts/host-event-contracts.d.ts +10 -0
- package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/cjs/src/css-variables.d.ts +373 -5
- package/cjs/src/css-variables.d.ts.map +1 -1
- package/cjs/src/embed/app.d.ts +15 -0
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js +6 -3
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/app.spec.js +28 -0
- package/cjs/src/embed/app.spec.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.js +244 -4
- package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.spec.js +389 -0
- package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/cjs/src/embed/bodyless-conversation.js +1 -1
- package/cjs/src/embed/bodyless-conversation.js.map +1 -1
- package/cjs/src/embed/conversation.d.ts +29 -0
- package/cjs/src/embed/conversation.d.ts.map +1 -1
- package/cjs/src/embed/conversation.js +7 -3
- package/cjs/src/embed/conversation.js.map +1 -1
- package/cjs/src/embed/conversation.spec.js +88 -0
- package/cjs/src/embed/conversation.spec.js.map +1 -1
- package/cjs/src/embed/host-events.spec.js +32 -0
- package/cjs/src/embed/host-events.spec.js.map +1 -1
- package/cjs/src/embed/liveboard-override-utils.d.ts +59 -0
- package/cjs/src/embed/liveboard-override-utils.d.ts.map +1 -0
- package/cjs/src/embed/liveboard-override-utils.js +19 -0
- package/cjs/src/embed/liveboard-override-utils.js.map +1 -0
- package/cjs/src/embed/liveboard-override-utils.spec.d.ts +2 -0
- package/cjs/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
- package/cjs/src/embed/liveboard-override-utils.spec.js +34 -0
- package/cjs/src/embed/liveboard-override-utils.spec.js.map +1 -0
- package/cjs/src/embed/liveboard.d.ts +44 -13
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +19 -23
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +122 -0
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/search-bar.js +1 -1
- package/cjs/src/embed/search-bar.js.map +1 -1
- package/cjs/src/embed/search.js +1 -1
- package/cjs/src/embed/search.js.map +1 -1
- package/cjs/src/embed/search.spec.js +20 -0
- package/cjs/src/embed/search.spec.js.map +1 -1
- package/cjs/src/embed/spotter-utils.d.ts +12 -0
- package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
- package/cjs/src/embed/spotter-utils.js +20 -2
- package/cjs/src/embed/spotter-utils.js.map +1 -1
- package/cjs/src/embed/spotter-utils.spec.js +19 -0
- package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
- package/cjs/src/embed/spotter-viz-utils.d.ts +15 -0
- package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -1
- package/cjs/src/embed/spotter-viz-utils.js.map +1 -1
- package/cjs/src/embed/spotter-viz-utils.spec.js +11 -0
- package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -1
- package/cjs/src/embed/ts-embed.d.ts +18 -20
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +181 -45
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +610 -0
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +5 -3
- package/cjs/src/index.d.ts.map +1 -1
- package/cjs/src/index.js +4 -2
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/mixpanel-service.d.ts +1 -0
- package/cjs/src/mixpanel-service.d.ts.map +1 -1
- package/cjs/src/mixpanel-service.js +1 -0
- package/cjs/src/mixpanel-service.js.map +1 -1
- package/cjs/src/react/all-types-export.d.ts +1 -1
- package/cjs/src/react/all-types-export.d.ts.map +1 -1
- package/cjs/src/react/all-types-export.js +2 -1
- package/cjs/src/react/all-types-export.js.map +1 -1
- package/cjs/src/types.d.ts +251 -4
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +176 -0
- package/cjs/src/types.js.map +1 -1
- package/cjs/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
- package/cjs/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
- package/cjs/src/utils/graphql/answerService/answer-queries.js +8 -1
- package/cjs/src/utils/graphql/answerService/answer-queries.js.map +1 -1
- package/cjs/src/utils/graphql/answerService/answerService.d.ts +28 -4
- package/cjs/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/cjs/src/utils/graphql/answerService/answerService.js +39 -4
- package/cjs/src/utils/graphql/answerService/answerService.js.map +1 -1
- package/cjs/src/utils/graphql/answerService/answerService.spec.js +124 -0
- package/cjs/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
- package/cjs/src/utils/runtimeTelemetry.d.ts +11 -0
- package/cjs/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.js +34 -0
- package/cjs/src/utils/runtimeTelemetry.js.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/cjs/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.spec.js +124 -0
- package/cjs/src/utils/runtimeTelemetry.spec.js.map +1 -0
- package/dist/{index-CnHREfvN.js → index-CrBnZBZv.js} +1 -1
- package/dist/src/contracts/host-event-contracts.d.ts +10 -0
- package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/dist/src/css-variables.d.ts +373 -5
- package/dist/src/css-variables.d.ts.map +1 -1
- package/dist/src/embed/app.d.ts +15 -0
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/dist/src/embed/conversation.d.ts +29 -0
- package/dist/src/embed/conversation.d.ts.map +1 -1
- package/dist/src/embed/liveboard-override-utils.d.ts +59 -0
- package/dist/src/embed/liveboard-override-utils.d.ts.map +1 -0
- package/dist/src/embed/liveboard-override-utils.spec.d.ts +2 -0
- package/dist/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
- package/dist/src/embed/liveboard.d.ts +44 -13
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/spotter-utils.d.ts +12 -0
- package/dist/src/embed/spotter-utils.d.ts.map +1 -1
- package/dist/src/embed/spotter-viz-utils.d.ts +15 -0
- package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -1
- package/dist/src/embed/ts-embed.d.ts +18 -20
- package/dist/src/embed/ts-embed.d.ts.map +1 -1
- package/dist/src/embed/ts-embed.spec.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/mixpanel-service.d.ts +1 -0
- package/dist/src/mixpanel-service.d.ts.map +1 -1
- package/dist/src/react/all-types-export.d.ts +1 -1
- package/dist/src/react/all-types-export.d.ts.map +1 -1
- package/dist/src/types.d.ts +251 -4
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
- package/dist/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
- package/dist/src/utils/graphql/answerService/answerService.d.ts +28 -4
- package/dist/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/dist/src/utils/runtimeTelemetry.d.ts +11 -0
- package/dist/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/dist/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/dist/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/dist/tsembed-react.es.js +500 -85
- package/dist/tsembed-react.js +499 -84
- package/dist/tsembed.es.js +743 -90
- package/dist/tsembed.js +741 -88
- package/dist/visual-embed-sdk-react-full.d.ts +788 -44
- package/dist/visual-embed-sdk-react.d.ts +783 -44
- package/dist/visual-embed-sdk.d.ts +815 -45
- package/lib/package.json +1 -1
- package/lib/src/contracts/contracts.spec.js +4 -0
- package/lib/src/contracts/contracts.spec.js.map +1 -1
- package/lib/src/contracts/host-event-contracts.d.ts +10 -0
- package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/lib/src/css-variables.d.ts +373 -5
- package/lib/src/css-variables.d.ts.map +1 -1
- package/lib/src/embed/app.d.ts +15 -0
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js +7 -4
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/app.spec.js +28 -0
- package/lib/src/embed/app.spec.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/lib/src/embed/auto-frame-renderer.js +244 -4
- package/lib/src/embed/auto-frame-renderer.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.spec.js +389 -0
- package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/lib/src/embed/bodyless-conversation.js +1 -1
- package/lib/src/embed/bodyless-conversation.js.map +1 -1
- package/lib/src/embed/conversation.d.ts +29 -0
- package/lib/src/embed/conversation.d.ts.map +1 -1
- package/lib/src/embed/conversation.js +8 -4
- package/lib/src/embed/conversation.js.map +1 -1
- package/lib/src/embed/conversation.spec.js +89 -1
- package/lib/src/embed/conversation.spec.js.map +1 -1
- package/lib/src/embed/host-events.spec.js +32 -0
- package/lib/src/embed/host-events.spec.js.map +1 -1
- package/lib/src/embed/liveboard-override-utils.d.ts +59 -0
- package/lib/src/embed/liveboard-override-utils.d.ts.map +1 -0
- package/lib/src/embed/liveboard-override-utils.js +15 -0
- package/lib/src/embed/liveboard-override-utils.js.map +1 -0
- package/lib/src/embed/liveboard-override-utils.spec.d.ts +2 -0
- package/lib/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
- package/lib/src/embed/liveboard-override-utils.spec.js +32 -0
- package/lib/src/embed/liveboard-override-utils.spec.js.map +1 -0
- package/lib/src/embed/liveboard.d.ts +44 -13
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +19 -23
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +122 -0
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/search-bar.js +1 -1
- package/lib/src/embed/search-bar.js.map +1 -1
- package/lib/src/embed/search.js +1 -1
- package/lib/src/embed/search.js.map +1 -1
- package/lib/src/embed/search.spec.js +20 -0
- package/lib/src/embed/search.spec.js.map +1 -1
- package/lib/src/embed/spotter-utils.d.ts +12 -0
- package/lib/src/embed/spotter-utils.d.ts.map +1 -1
- package/lib/src/embed/spotter-utils.js +19 -1
- package/lib/src/embed/spotter-utils.js.map +1 -1
- package/lib/src/embed/spotter-utils.spec.js +19 -0
- package/lib/src/embed/spotter-utils.spec.js.map +1 -1
- package/lib/src/embed/spotter-viz-utils.d.ts +15 -0
- package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -1
- package/lib/src/embed/spotter-viz-utils.js.map +1 -1
- package/lib/src/embed/spotter-viz-utils.spec.js +11 -0
- package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -1
- package/lib/src/embed/ts-embed.d.ts +18 -20
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +181 -45
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +611 -1
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +5 -3
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +3 -2
- package/lib/src/index.js.map +1 -1
- package/lib/src/mixpanel-service.d.ts +1 -0
- package/lib/src/mixpanel-service.d.ts.map +1 -1
- package/lib/src/mixpanel-service.js +1 -0
- package/lib/src/mixpanel-service.js.map +1 -1
- package/lib/src/react/all-types-export.d.ts +1 -1
- package/lib/src/react/all-types-export.d.ts.map +1 -1
- package/lib/src/react/all-types-export.js +1 -1
- package/lib/src/react/all-types-export.js.map +1 -1
- package/lib/src/types.d.ts +251 -4
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +176 -0
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
- package/lib/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
- package/lib/src/utils/graphql/answerService/answer-queries.js +7 -0
- package/lib/src/utils/graphql/answerService/answer-queries.js.map +1 -1
- package/lib/src/utils/graphql/answerService/answerService.d.ts +28 -4
- package/lib/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/lib/src/utils/graphql/answerService/answerService.js +39 -4
- package/lib/src/utils/graphql/answerService/answerService.js.map +1 -1
- package/lib/src/utils/graphql/answerService/answerService.spec.js +125 -1
- package/lib/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
- package/lib/src/utils/runtimeTelemetry.d.ts +11 -0
- package/lib/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/lib/src/utils/runtimeTelemetry.js +28 -0
- package/lib/src/utils/runtimeTelemetry.js.map +1 -0
- package/lib/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/lib/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/lib/src/utils/runtimeTelemetry.spec.js +122 -0
- package/lib/src/utils/runtimeTelemetry.spec.js.map +1 -0
- package/lib/src/visual-embed-sdk.d.ts +815 -45
- package/package.json +1 -1
- package/src/contracts/contracts.spec.ts +4 -0
- package/src/contracts/host-event-contracts.ts +13 -0
- package/src/css-variables.ts +475 -26
- package/src/embed/app.spec.ts +32 -0
- package/src/embed/app.ts +29 -4
- package/src/embed/auto-frame-renderer.spec.ts +464 -0
- package/src/embed/auto-frame-renderer.ts +305 -6
- package/src/embed/bodyless-conversation.ts +1 -1
- package/src/embed/conversation.spec.ts +118 -1
- package/src/embed/conversation.ts +42 -3
- package/src/embed/host-events.spec.ts +44 -0
- package/src/embed/liveboard-override-utils.spec.ts +38 -0
- package/src/embed/liveboard-override-utils.ts +71 -0
- package/src/embed/liveboard.spec.ts +138 -0
- package/src/embed/liveboard.ts +64 -24
- package/src/embed/search-bar.tsx +1 -1
- package/src/embed/search.spec.ts +23 -0
- package/src/embed/search.ts +1 -1
- package/src/embed/spotter-utils.spec.ts +20 -0
- package/src/embed/spotter-utils.ts +24 -1
- package/src/embed/spotter-viz-utils.spec.ts +11 -0
- package/src/embed/spotter-viz-utils.ts +15 -0
- package/src/embed/ts-embed.spec.ts +767 -0
- package/src/embed/ts-embed.ts +186 -41
- package/src/index.ts +18 -1
- package/src/mixpanel-service.ts +1 -0
- package/src/react/all-types-export.ts +7 -0
- package/src/types.ts +254 -5
- package/src/utils/graphql/answerService/answer-queries.ts +8 -0
- package/src/utils/graphql/answerService/answerService.spec.ts +145 -1
- package/src/utils/graphql/answerService/answerService.ts +65 -3
- package/src/utils/runtimeTelemetry.spec.ts +148 -0
- package/src/utils/runtimeTelemetry.ts +50 -0
package/src/embed/app.spec.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ListPage,
|
|
9
9
|
} from './app';
|
|
10
10
|
import { SpotterQueryMode } from './conversation';
|
|
11
|
+
import { SpotterExperienceVersion } from './spotter-utils';
|
|
11
12
|
import { init } from '../index';
|
|
12
13
|
import { Action, AuthType, EmbedEvent, HostEvent, RuntimeFilterOp } from '../types';
|
|
13
14
|
import {
|
|
@@ -3081,3 +3082,34 @@ describe('AppEmbed updatedSpotterExperience tests', () => {
|
|
|
3081
3082
|
});
|
|
3082
3083
|
});
|
|
3083
3084
|
});
|
|
3085
|
+
|
|
3086
|
+
describe('AppEmbed spotterExperienceVersion tests', () => {
|
|
3087
|
+
beforeEach(() => {
|
|
3088
|
+
cleanUp();
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3091
|
+
const renderWithConfig = async (overrides: Partial<AppViewConfig>) => {
|
|
3092
|
+
const appEmbed = new AppEmbed(getRootEl(), {
|
|
3093
|
+
...defaultViewConfig,
|
|
3094
|
+
...overrides,
|
|
3095
|
+
} as AppViewConfig);
|
|
3096
|
+
appEmbed.render();
|
|
3097
|
+
let src = '';
|
|
3098
|
+
await executeAfterWait(() => {
|
|
3099
|
+
src = getIFrameSrc();
|
|
3100
|
+
});
|
|
3101
|
+
return src;
|
|
3102
|
+
};
|
|
3103
|
+
|
|
3104
|
+
test('should add the spotterExperienceVersion param when spotterExperienceVersion is set', async () => {
|
|
3105
|
+
const src = await renderWithConfig({
|
|
3106
|
+
spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
|
|
3107
|
+
});
|
|
3108
|
+
expect(src).toContain(`spotterExperienceVersion=${SpotterExperienceVersion.SPOTTER_2026_11}`);
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
test('should not add the spotterExperienceVersion param when spotterExperienceVersion is not set', async () => {
|
|
3112
|
+
const src = await renderWithConfig({});
|
|
3113
|
+
expect(src).not.toContain('spotterExperienceVersion=');
|
|
3114
|
+
});
|
|
3115
|
+
});
|
package/src/embed/app.ts
CHANGED
|
@@ -24,7 +24,12 @@ import {
|
|
|
24
24
|
import { FullHeightController } from '../full-height';
|
|
25
25
|
import { V1Embed } from './ts-embed';
|
|
26
26
|
import { SpotterChatViewConfig, SpotterSidebarViewConfig, SpotterQueryMode, SpotterShareConversationConfig, StarterPromptsConfig } from './conversation';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
buildSpotterSidebarAppInitData,
|
|
29
|
+
buildSpotterShareConversationAppInitData,
|
|
30
|
+
buildStarterPromptsAppInitData,
|
|
31
|
+
SpotterExperienceVersion,
|
|
32
|
+
} from './spotter-utils';
|
|
28
33
|
import { SpotterVizConfig, buildSpotterVizAppInitData } from './spotter-viz-utils';
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -921,6 +926,20 @@ export interface AppViewConfig extends AllEmbedViewConfig, FullHeightViewConfig
|
|
|
921
926
|
* ```
|
|
922
927
|
*/
|
|
923
928
|
hideAnswerEditPanel?: boolean;
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* The Spotter experience version to load for the Spotter surface shown within
|
|
932
|
+
* this embed.
|
|
933
|
+
* @version SDK: 1.53.0 | ThoughtSpot Cloud: 26.11.0.cl
|
|
934
|
+
* @example
|
|
935
|
+
* ```js
|
|
936
|
+
* const embed = new AppEmbed('#tsEmbed', {
|
|
937
|
+
* ... // other options
|
|
938
|
+
* spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
|
|
939
|
+
* });
|
|
940
|
+
* ```
|
|
941
|
+
*/
|
|
942
|
+
spotterExperienceVersion?: SpotterExperienceVersion;
|
|
924
943
|
}
|
|
925
944
|
|
|
926
945
|
/**
|
|
@@ -933,6 +952,7 @@ export interface AppEmbedAppInitData extends DefaultAppInitData {
|
|
|
933
952
|
spotterVizConfig?: SpotterVizConfig;
|
|
934
953
|
spotterShareConversationConfig?: SpotterShareConversationConfig;
|
|
935
954
|
starterPrompts?: StarterPromptsConfig;
|
|
955
|
+
enableSpotterInstructionAndMoreSettings?: boolean;
|
|
936
956
|
};
|
|
937
957
|
}
|
|
938
958
|
|
|
@@ -991,7 +1011,7 @@ export class AppEmbed extends V1Embed {
|
|
|
991
1011
|
* embedded Liveboard or visualization.
|
|
992
1012
|
*/
|
|
993
1013
|
protected getEmbedParams() {
|
|
994
|
-
const params = this.
|
|
1014
|
+
const params = this.getUrlQueryParamsObject();
|
|
995
1015
|
return getQueryParamString(params, true);
|
|
996
1016
|
}
|
|
997
1017
|
|
|
@@ -1057,6 +1077,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1057
1077
|
isContinuousLiveboardPDFEnabled,
|
|
1058
1078
|
enableLiveboardDataCache,
|
|
1059
1079
|
hideAnswerEditPanel,
|
|
1080
|
+
spotterExperienceVersion,
|
|
1060
1081
|
} = this.viewConfig;
|
|
1061
1082
|
|
|
1062
1083
|
let params: any = {};
|
|
@@ -1117,6 +1138,10 @@ export class AppEmbed extends V1Embed {
|
|
|
1117
1138
|
params[Param.SpotterDataSources] = JSON.stringify(spotterDataSources);
|
|
1118
1139
|
}
|
|
1119
1140
|
|
|
1141
|
+
if (spotterExperienceVersion !== undefined) {
|
|
1142
|
+
params[Param.SpotterExperienceVersion] = spotterExperienceVersion;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1120
1145
|
// Handle spotterChatConfig params
|
|
1121
1146
|
if (spotterChatConfig) {
|
|
1122
1147
|
const {
|
|
@@ -1281,7 +1306,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1281
1306
|
|
|
1282
1307
|
// Navigation V1/V2 and home page V1/V2 are deprecated. V3 is now the
|
|
1283
1308
|
// default experience, Need to send navigationVersion=v3 and
|
|
1284
|
-
// homepageVersion=v3 even when the no discoveryExperience /
|
|
1309
|
+
// homepageVersion=v3 even when the no discoveryExperience /
|
|
1285
1310
|
// modularHomeExperience config is set. Without these
|
|
1286
1311
|
// params, older TSA versions (< 26.7) fall back to V2 for nav and
|
|
1287
1312
|
// home page, so the defaults must be sent explicitly from the SDK.
|
|
@@ -1293,7 +1318,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1293
1318
|
if (discoveryExperience.listPageVersion !== undefined) {
|
|
1294
1319
|
params[Param.ListPageVersion] = discoveryExperience.listPageVersion;
|
|
1295
1320
|
}
|
|
1296
|
-
|
|
1321
|
+
|
|
1297
1322
|
if (discoveryExperience.homePage === HomePage.Focused) {
|
|
1298
1323
|
params[Param.HomepageVersion] = HomePage.Focused;
|
|
1299
1324
|
// The Focused (V4) homepage experience requires the updated
|
|
@@ -673,4 +673,468 @@ describe('startAutoMCPFrameRenderer', () => {
|
|
|
673
673
|
expect(JSON.parse(autoValue)).toEqual(JSON.parse(liveboardValue));
|
|
674
674
|
});
|
|
675
675
|
});
|
|
676
|
+
|
|
677
|
+
// ─── replaying a stored conversation
|
|
678
|
+
// ──────────────────────────────────────
|
|
679
|
+
|
|
680
|
+
describe('stored conversation replay', () => {
|
|
681
|
+
const CONVERSATION_ID = 'U0Tfqmjlz6WT';
|
|
682
|
+
const REPLAY_SRC =
|
|
683
|
+
`https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
684
|
+
+ `&${Param.TsmcpConversationId}=${CONVERSATION_ID}`
|
|
685
|
+
+ `&${Param.TsmcpAnswerIndex}=1`;
|
|
686
|
+
|
|
687
|
+
/** Conversation-service shape: two real answers around a thinking one. */
|
|
688
|
+
const messagesPayload = {
|
|
689
|
+
messages: [
|
|
690
|
+
{
|
|
691
|
+
message_id: 'm1',
|
|
692
|
+
response_items: [
|
|
693
|
+
{ type: 'tool_call', is_thinking: false },
|
|
694
|
+
{ type: 'answer', answer_id: 'ans_first', is_thinking: false },
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
message_id: 'm2',
|
|
699
|
+
response_items: [
|
|
700
|
+
{ type: 'answer', answer_id: 'ans_thinking', is_thinking: true },
|
|
701
|
+
{ type: 'answer', answer_id: 'ans_second', is_thinking: false },
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const detailsPayload = {
|
|
708
|
+
answer: {
|
|
709
|
+
session_identifier: 'fresh-session',
|
|
710
|
+
generation_number: 3,
|
|
711
|
+
ac_state: { transaction_identifier: 'fresh-ac', generation_number: 2 },
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
let fetchMock: jest.SpyInstance;
|
|
716
|
+
|
|
717
|
+
/** Renders one replay iframe and returns the src plus the frame element. */
|
|
718
|
+
async function renderReplayFrame(
|
|
719
|
+
viewConfig: AutoMCPFrameRendererViewConfig = {},
|
|
720
|
+
src: string = REPLAY_SRC,
|
|
721
|
+
): Promise<{ renderedSrc: string; frame: HTMLIFrameElement }> {
|
|
722
|
+
let renderedSrc = '';
|
|
723
|
+
let frame: HTMLIFrameElement;
|
|
724
|
+
renderIFrameSpy.mockRestore();
|
|
725
|
+
renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
|
|
726
|
+
.mockImplementation(async function (this: any, capturedSrc: string) {
|
|
727
|
+
renderedSrc = capturedSrc;
|
|
728
|
+
// Stand in for the real iframe the notice attaches to.
|
|
729
|
+
frame = document.createElement('iframe');
|
|
730
|
+
this.iFrame = frame;
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const observer = startAutoMCPFrameRenderer(viewConfig);
|
|
734
|
+
const iframe = document.createElement('iframe');
|
|
735
|
+
iframe.src = src;
|
|
736
|
+
document.body.appendChild(iframe);
|
|
737
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
738
|
+
observer.disconnect();
|
|
739
|
+
return { renderedSrc, frame: frame! };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
beforeEach(() => {
|
|
743
|
+
fetchMock = jest.spyOn(global, 'fetch').mockImplementation(async (input: any) => {
|
|
744
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
745
|
+
const body = url.includes('/load/public') ? detailsPayload : messagesPayload;
|
|
746
|
+
return { ok: true, status: 200, json: async () => body } as any;
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
afterEach(() => {
|
|
751
|
+
fetchMock.mockRestore();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test('resolves session params from the conversation and answer APIs', async () => {
|
|
755
|
+
const { renderedSrc } = await renderReplayFrame();
|
|
756
|
+
|
|
757
|
+
expect(renderedSrc).toContain('sessionId=fresh-session');
|
|
758
|
+
expect(renderedSrc).toContain('genNo=3');
|
|
759
|
+
expect(renderedSrc).toContain('acSessionId=fresh-ac');
|
|
760
|
+
expect(renderedSrc).toContain('acGenNo=2');
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test('picks the answer at the requested index, skipping thinking items', async () => {
|
|
764
|
+
await renderReplayFrame();
|
|
765
|
+
|
|
766
|
+
const detailsCall = fetchMock.mock.calls
|
|
767
|
+
.map(([input]: any) => (typeof input === 'string' ? input : input.url))
|
|
768
|
+
.find((url: string) => url.includes('/load/public'));
|
|
769
|
+
// index 1 = second non-thinking answer; ans_thinking must not
|
|
770
|
+
// count.
|
|
771
|
+
expect(detailsCall).toContain('/message/ans_second/load/public');
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test('defaults to the conversational answer route when none is given', async () => {
|
|
775
|
+
const { renderedSrc } = await renderReplayFrame();
|
|
776
|
+
expect(renderedSrc).toContain('/embed/conv-assist-answer');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('strips the replay markers from the URL sent to ThoughtSpot', async () => {
|
|
780
|
+
const { renderedSrc } = await renderReplayFrame();
|
|
781
|
+
expect(renderedSrc).not.toContain(Param.TsmcpConversationId);
|
|
782
|
+
expect(renderedSrc).not.toContain(Param.TsmcpAnswerIndex);
|
|
783
|
+
expect(renderedSrc).not.toContain(`${Param.Tsmcp}=true`);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test('marks a replayed answer with the stale-data notice', async () => {
|
|
787
|
+
const { frame } = await renderReplayFrame();
|
|
788
|
+
expect(frame.dataset.tsStaleAnswer).toBe('true');
|
|
789
|
+
expect(frame.title).toBe(
|
|
790
|
+
'This data may have changed since the last time you had a chat.',
|
|
791
|
+
);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test('suppressStaleAnswerNotice leaves the frame unmarked', async () => {
|
|
795
|
+
const { frame } = await renderReplayFrame({ suppressStaleAnswerNotice: true });
|
|
796
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
797
|
+
expect(frame.title).toBe('');
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('staleAnswerNoticeText overrides the default copy', async () => {
|
|
801
|
+
const { frame } = await renderReplayFrame({ staleAnswerNoticeText: 'Numbers may differ.' });
|
|
802
|
+
expect(frame.title).toBe('Numbers may differ.');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test('falls back to the source URL when resolution fails', async () => {
|
|
806
|
+
fetchMock.mockImplementation(async () => ({ ok: false, status: 500 } as any));
|
|
807
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
808
|
+
|
|
809
|
+
// No invented params, and no notice claiming a successful refresh.
|
|
810
|
+
expect(renderedSrc).toBe(REPLAY_SRC);
|
|
811
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
812
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test.each([
|
|
816
|
+
['a null messages body', null],
|
|
817
|
+
['a messages body that is not an object', 'nope'],
|
|
818
|
+
['messages that is not an array', { messages: null }],
|
|
819
|
+
])('falls back on %s instead of throwing', async (_label, body) => {
|
|
820
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
821
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
822
|
+
return {
|
|
823
|
+
ok: true,
|
|
824
|
+
status: 200,
|
|
825
|
+
json: async () => (url.includes('/load/public') ? detailsPayload : body),
|
|
826
|
+
} as any;
|
|
827
|
+
});
|
|
828
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
829
|
+
|
|
830
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
831
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('falls back on a null details body instead of throwing', async () => {
|
|
835
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
836
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
837
|
+
return {
|
|
838
|
+
ok: true,
|
|
839
|
+
status: 200,
|
|
840
|
+
json: async () => (url.includes('/load/public') ? null : messagesPayload),
|
|
841
|
+
} as any;
|
|
842
|
+
});
|
|
843
|
+
const { renderedSrc } = await renderReplayFrame();
|
|
844
|
+
|
|
845
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test.each([
|
|
849
|
+
['answer.generation_number', { ...detailsPayload.answer, generation_number: null }],
|
|
850
|
+
[
|
|
851
|
+
'ac_state.generation_number',
|
|
852
|
+
{ ...detailsPayload.answer, ac_state: { transaction_identifier: 'fresh-ac', generation_number: null } },
|
|
853
|
+
],
|
|
854
|
+
])('treats a null %s as unresolvable', async (_label, answer) => {
|
|
855
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
856
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
857
|
+
return {
|
|
858
|
+
ok: true,
|
|
859
|
+
status: 200,
|
|
860
|
+
json: async () => (url.includes('/load/public') ? { answer } : messagesPayload),
|
|
861
|
+
} as any;
|
|
862
|
+
});
|
|
863
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
864
|
+
|
|
865
|
+
// Never emit a literal "null" generation into the hash.
|
|
866
|
+
expect(renderedSrc).not.toContain('genNo=null');
|
|
867
|
+
expect(renderedSrc).not.toContain('acGenNo=null');
|
|
868
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
869
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test.each([
|
|
873
|
+
['a fractional index', '1.5'],
|
|
874
|
+
['a negative index', '-1'],
|
|
875
|
+
['a malformed index', 'abc'],
|
|
876
|
+
['a trailing-junk index', '1abc'],
|
|
877
|
+
])('%s falls back to the first answer', async (_label, value) => {
|
|
878
|
+
const src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
879
|
+
+ `&${Param.TsmcpConversationId}=${CONVERSATION_ID}`
|
|
880
|
+
+ `&${Param.TsmcpAnswerIndex}=${value}`;
|
|
881
|
+
await renderReplayFrame({}, src);
|
|
882
|
+
|
|
883
|
+
const detailsCall = fetchMock.mock.calls
|
|
884
|
+
.map(([input]: any) => (typeof input === 'string' ? input : input.url))
|
|
885
|
+
.find((url: string) => url.includes('/load/public'));
|
|
886
|
+
expect(detailsCall).toContain('/message/ans_first/load/public');
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
/** URLs passed to fetch, in call order. */
|
|
890
|
+
function fetchedUrls(): string[] {
|
|
891
|
+
return fetchMock.mock.calls
|
|
892
|
+
.map(([input]: any) => (typeof input === 'string' ? input : input.url));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Serves `messages` for the list call and `details` for the load call. */
|
|
896
|
+
function mockConversation(messages: any, details: any = detailsPayload) {
|
|
897
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
898
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
899
|
+
return {
|
|
900
|
+
ok: true,
|
|
901
|
+
status: 200,
|
|
902
|
+
json: async () => (url.includes('/load/public') ? details : messages),
|
|
903
|
+
} as any;
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
test('calls the conversation service with the encoded conversation id', async () => {
|
|
908
|
+
const src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
909
|
+
+ `&${Param.TsmcpConversationId}=${encodeURIComponent('conv/1')}`;
|
|
910
|
+
await renderReplayFrame({}, src);
|
|
911
|
+
|
|
912
|
+
const [listUrl, loadUrl] = fetchedUrls();
|
|
913
|
+
expect(listUrl).toBe(`http://${thoughtSpotHost}/conversation/v2/conv%2F1/public`);
|
|
914
|
+
expect(loadUrl).toBe(
|
|
915
|
+
`http://${thoughtSpotHost}/conversation/v2/conv%2F1/message/ans_first/load/public`,
|
|
916
|
+
);
|
|
917
|
+
const loadInit = fetchMock.mock.calls[1][1];
|
|
918
|
+
expect(loadInit.method).toBe('POST');
|
|
919
|
+
expect(JSON.parse(loadInit.body)).toEqual({ type: 'TS_ANSWER' });
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test('defaults to the first answer when no index is given', async () => {
|
|
923
|
+
const src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
924
|
+
+ `&${Param.TsmcpConversationId}=${CONVERSATION_ID}`;
|
|
925
|
+
await renderReplayFrame({}, src);
|
|
926
|
+
|
|
927
|
+
expect(fetchedUrls()[1]).toContain('/message/ans_first/load/public');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test('keeps the source route and its other hash params', async () => {
|
|
931
|
+
const src = `${REPLAY_SRC}#/embed/viz/lb1?keep=yes&sessionId=expired`;
|
|
932
|
+
const { renderedSrc } = await renderReplayFrame({}, src);
|
|
933
|
+
|
|
934
|
+
expect(renderedSrc).toContain('/embed/viz/lb1?');
|
|
935
|
+
expect(renderedSrc).not.toContain('/embed/conv-assist-answer');
|
|
936
|
+
expect(renderedSrc).toContain('keep=yes');
|
|
937
|
+
expect(renderedSrc).toContain('sessionId=fresh-session');
|
|
938
|
+
expect(renderedSrc).not.toContain('sessionId=expired');
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('ignores messages without response items and answers without an id', async () => {
|
|
942
|
+
mockConversation({
|
|
943
|
+
messages: [
|
|
944
|
+
{ message_id: 'm0' },
|
|
945
|
+
null,
|
|
946
|
+
{ message_id: 'm1', response_items: [{ type: 'answer', is_thinking: false }] },
|
|
947
|
+
{
|
|
948
|
+
message_id: 'm2',
|
|
949
|
+
response_items: [null, { type: 'answer', answer_id: 'ans_only', is_thinking: false }],
|
|
950
|
+
},
|
|
951
|
+
],
|
|
952
|
+
});
|
|
953
|
+
const src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
954
|
+
+ `&${Param.TsmcpConversationId}=${CONVERSATION_ID}`;
|
|
955
|
+
const { renderedSrc } = await renderReplayFrame({}, src);
|
|
956
|
+
|
|
957
|
+
expect(fetchedUrls()[1]).toContain('/message/ans_only/load/public');
|
|
958
|
+
expect(renderedSrc).toContain('sessionId=fresh-session');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test('falls back when no answer exists at the requested index', async () => {
|
|
962
|
+
const src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
963
|
+
+ `&${Param.TsmcpConversationId}=${CONVERSATION_ID}`
|
|
964
|
+
+ `&${Param.TsmcpAnswerIndex}=5`;
|
|
965
|
+
const { renderedSrc, frame } = await renderReplayFrame({}, src);
|
|
966
|
+
|
|
967
|
+
// The list, read once more in case it predated the answer; there is
|
|
968
|
+
// no answer to load.
|
|
969
|
+
expect(fetchedUrls()).toHaveLength(2);
|
|
970
|
+
expect(fetchedUrls().every((url) => url.endsWith('/public') && !url.includes('/load/'))).toBe(true);
|
|
971
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
972
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
test('falls back when the answer load fails', async () => {
|
|
976
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
977
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
978
|
+
if (url.includes('/load/public')) {
|
|
979
|
+
return { ok: false, status: 404 } as any;
|
|
980
|
+
}
|
|
981
|
+
return { ok: true, status: 200, json: async () => messagesPayload } as any;
|
|
982
|
+
});
|
|
983
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
984
|
+
|
|
985
|
+
expect(fetchedUrls()).toHaveLength(2);
|
|
986
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
987
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test('falls back when a fetch throws', async () => {
|
|
991
|
+
fetchMock.mockImplementation(async () => {
|
|
992
|
+
throw new Error('network down');
|
|
993
|
+
});
|
|
994
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
995
|
+
|
|
996
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
997
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test.each([
|
|
1001
|
+
['session_identifier', { ...detailsPayload.answer, session_identifier: '' }],
|
|
1002
|
+
[
|
|
1003
|
+
'ac_state.transaction_identifier',
|
|
1004
|
+
{ ...detailsPayload.answer, ac_state: { generation_number: 2 } },
|
|
1005
|
+
],
|
|
1006
|
+
['ac_state', { ...detailsPayload.answer, ac_state: undefined }],
|
|
1007
|
+
['a positive generation_number', { ...detailsPayload.answer, generation_number: 0 }],
|
|
1008
|
+
])('treats a missing %s as unresolvable', async (_label, answer) => {
|
|
1009
|
+
mockConversation(messagesPayload, { answer });
|
|
1010
|
+
const { renderedSrc, frame } = await renderReplayFrame();
|
|
1011
|
+
|
|
1012
|
+
expect(renderedSrc).not.toContain('sessionId=');
|
|
1013
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test('skips the notice when no iframe was rendered', async () => {
|
|
1017
|
+
renderIFrameSpy.mockRestore();
|
|
1018
|
+
renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
|
|
1019
|
+
.mockImplementation(async function (this: any) {
|
|
1020
|
+
this.iFrame = undefined;
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const observer = startAutoMCPFrameRenderer({});
|
|
1024
|
+
const iframe = document.createElement('iframe');
|
|
1025
|
+
iframe.src = REPLAY_SRC;
|
|
1026
|
+
document.body.appendChild(iframe);
|
|
1027
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1028
|
+
observer.disconnect();
|
|
1029
|
+
|
|
1030
|
+
// Resolution ran, and marking a missing frame did not throw.
|
|
1031
|
+
expect(fetchedUrls()).toHaveLength(2);
|
|
1032
|
+
expect(renderIFrameSpy).toHaveBeenCalledTimes(1);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
describe('sharing the answer-id list', () => {
|
|
1036
|
+
const replaySrc = (index: number, conversationId = CONVERSATION_ID) =>
|
|
1037
|
+
`https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true`
|
|
1038
|
+
+ `&${Param.TsmcpConversationId}=${conversationId}`
|
|
1039
|
+
+ `&${Param.TsmcpAnswerIndex}=${index}`;
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Mounts all `srcs` in one batch, as a replayed chat does.
|
|
1043
|
+
* Returns the rendered srcs.
|
|
1044
|
+
*/
|
|
1045
|
+
async function renderReplayBatch(srcs: string[]): Promise<string[]> {
|
|
1046
|
+
const rendered: string[] = [];
|
|
1047
|
+
renderIFrameSpy.mockRestore();
|
|
1048
|
+
renderIFrameSpy = jest.spyOn(TsEmbed.prototype as any, 'renderIFrame')
|
|
1049
|
+
.mockImplementation(async function (this: any, capturedSrc: string) {
|
|
1050
|
+
rendered.push(capturedSrc);
|
|
1051
|
+
this.iFrame = document.createElement('iframe');
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const observer = startAutoMCPFrameRenderer({});
|
|
1055
|
+
const container = document.createElement('div');
|
|
1056
|
+
srcs.forEach((src) => {
|
|
1057
|
+
const iframe = document.createElement('iframe');
|
|
1058
|
+
iframe.src = src;
|
|
1059
|
+
container.appendChild(iframe);
|
|
1060
|
+
});
|
|
1061
|
+
document.body.appendChild(container);
|
|
1062
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1063
|
+
observer.disconnect();
|
|
1064
|
+
return rendered;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const listCalls = () => fetchedUrls().filter((url) => !url.includes('/load/'));
|
|
1068
|
+
const loadCalls = () => fetchedUrls().filter((url) => url.includes('/load/'));
|
|
1069
|
+
|
|
1070
|
+
test('concurrent frames from one conversation share one list call', async () => {
|
|
1071
|
+
const rendered = await renderReplayBatch([replaySrc(0), replaySrc(1)]);
|
|
1072
|
+
|
|
1073
|
+
expect(listCalls()).toHaveLength(1);
|
|
1074
|
+
// Each answer still loads its own live session.
|
|
1075
|
+
expect(loadCalls()).toHaveLength(2);
|
|
1076
|
+
expect(loadCalls().some((url) => url.includes('/message/ans_first/'))).toBe(true);
|
|
1077
|
+
expect(loadCalls().some((url) => url.includes('/message/ans_second/'))).toBe(true);
|
|
1078
|
+
expect(rendered.every((src) => src.includes('sessionId=fresh-session'))).toBe(true);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test('frames from different conversations do not share a list', async () => {
|
|
1082
|
+
await renderReplayBatch([replaySrc(0), replaySrc(0, 'otherConversation')]);
|
|
1083
|
+
|
|
1084
|
+
expect(listCalls()).toHaveLength(2);
|
|
1085
|
+
expect(listCalls().some((url) => url.includes('/otherConversation/'))).toBe(true);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test('a failed list is not reused by a later frame', async () => {
|
|
1089
|
+
fetchMock.mockImplementation(async () => ({ ok: false, status: 500 } as any));
|
|
1090
|
+
await renderReplayBatch([replaySrc(0)]);
|
|
1091
|
+
expect(listCalls()).toHaveLength(1);
|
|
1092
|
+
|
|
1093
|
+
mockConversation(messagesPayload);
|
|
1094
|
+
const rendered = await renderReplayBatch([replaySrc(0)]);
|
|
1095
|
+
|
|
1096
|
+
expect(listCalls()).toHaveLength(2);
|
|
1097
|
+
expect(rendered[0]).toContain('sessionId=fresh-session');
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
test('a frame mounted after the batch settles reads a fresh list', async () => {
|
|
1101
|
+
await renderReplayBatch([replaySrc(0)]);
|
|
1102
|
+
await renderReplayBatch([replaySrc(1)]);
|
|
1103
|
+
|
|
1104
|
+
expect(listCalls()).toHaveLength(2);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test('a list too short for the index is read exactly once more', async () => {
|
|
1108
|
+
// The first read predates the second answer; the
|
|
1109
|
+
// second read has it.
|
|
1110
|
+
let listReads = 0;
|
|
1111
|
+
fetchMock.mockImplementation(async (input: any) => {
|
|
1112
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
1113
|
+
if (url.includes('/load/public')) {
|
|
1114
|
+
return { ok: true, status: 200, json: async () => detailsPayload } as any;
|
|
1115
|
+
}
|
|
1116
|
+
listReads += 1;
|
|
1117
|
+
const messages = listReads === 1
|
|
1118
|
+
? { messages: [messagesPayload.messages[0]] }
|
|
1119
|
+
: messagesPayload;
|
|
1120
|
+
return { ok: true, status: 200, json: async () => messages } as any;
|
|
1121
|
+
});
|
|
1122
|
+
const rendered = await renderReplayBatch([replaySrc(1), replaySrc(1)]);
|
|
1123
|
+
|
|
1124
|
+
// Both frames missed on the first list and joined one re-read.
|
|
1125
|
+
expect(listCalls()).toHaveLength(2);
|
|
1126
|
+
expect(loadCalls()).toHaveLength(2);
|
|
1127
|
+
expect(loadCalls().every((url) => url.includes('/message/ans_second/'))).toBe(true);
|
|
1128
|
+
expect(rendered.every((src) => src.includes('sessionId=fresh-session'))).toBe(true);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test('a frame without a conversation id makes no API calls and is unmarked', async () => {
|
|
1133
|
+
const { renderedSrc, frame } = await renderReplayFrame({}, TSMCP_SRC);
|
|
1134
|
+
|
|
1135
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1136
|
+
expect(renderedSrc).toContain('/embed/viz/lb1');
|
|
1137
|
+
expect(frame.dataset.tsStaleAnswer).toBeUndefined();
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
676
1140
|
});
|