@thoughtspot/visual-embed-sdk 1.52.2-beta.2 → 1.53.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/css-variables.d.ts +373 -5
- package/cjs/src/css-variables.d.ts.map +1 -1
- package/cjs/src/embed/app.d.ts +14 -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 +15 -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 +66 -0
- package/cjs/src/embed/conversation.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 +58 -13
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +11 -34
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +104 -79
- 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 +11 -0
- package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
- package/cjs/src/embed/spotter-utils.js +13 -1
- package/cjs/src/embed/spotter-utils.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 +27 -34
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +224 -102
- 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 +543 -166
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +4 -2
- 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 +182 -2
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +109 -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/dist/{index-CnHREfvN.js → index-CyDR8j_0.js} +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 +14 -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 +15 -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 +58 -13
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/spotter-utils.d.ts +11 -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 +27 -34
- 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 +4 -2
- 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 +182 -2
- 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/tsembed-react.es.js +437 -191
- package/dist/tsembed-react.js +436 -190
- package/dist/tsembed.es.js +680 -196
- package/dist/tsembed.js +678 -194
- package/dist/visual-embed-sdk-react-full.d.ts +702 -56
- package/dist/visual-embed-sdk-react.d.ts +697 -56
- package/dist/visual-embed-sdk.d.ts +744 -57
- package/lib/package.json +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 +14 -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 +15 -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 +67 -1
- package/lib/src/embed/conversation.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 +58 -13
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +11 -33
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +104 -79
- 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 +11 -0
- package/lib/src/embed/spotter-utils.d.ts.map +1 -1
- package/lib/src/embed/spotter-utils.js +12 -0
- package/lib/src/embed/spotter-utils.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 +27 -34
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +224 -102
- 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 +544 -167
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +4 -2
- 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 +182 -2
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +109 -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/visual-embed-sdk.d.ts +744 -57
- package/package.json +1 -1
- package/src/css-variables.ts +475 -26
- package/src/embed/app.spec.ts +32 -0
- package/src/embed/app.ts +28 -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 +94 -1
- package/src/embed/conversation.ts +28 -3
- 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 +119 -92
- package/src/embed/liveboard.ts +56 -40
- 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.ts +12 -0
- 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 +685 -207
- package/src/embed/ts-embed.ts +227 -97
- package/src/index.ts +16 -1
- package/src/mixpanel-service.ts +1 -0
- package/src/react/all-types-export.ts +6 -0
- package/src/types.ts +186 -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/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
|
/**
|
|
@@ -991,7 +1010,7 @@ export class AppEmbed extends V1Embed {
|
|
|
991
1010
|
* embedded Liveboard or visualization.
|
|
992
1011
|
*/
|
|
993
1012
|
protected getEmbedParams() {
|
|
994
|
-
const params = this.
|
|
1013
|
+
const params = this.getUrlQueryParamsObject();
|
|
995
1014
|
return getQueryParamString(params, true);
|
|
996
1015
|
}
|
|
997
1016
|
|
|
@@ -1057,6 +1076,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1057
1076
|
isContinuousLiveboardPDFEnabled,
|
|
1058
1077
|
enableLiveboardDataCache,
|
|
1059
1078
|
hideAnswerEditPanel,
|
|
1079
|
+
spotterExperienceVersion,
|
|
1060
1080
|
} = this.viewConfig;
|
|
1061
1081
|
|
|
1062
1082
|
let params: any = {};
|
|
@@ -1117,6 +1137,10 @@ export class AppEmbed extends V1Embed {
|
|
|
1117
1137
|
params[Param.SpotterDataSources] = JSON.stringify(spotterDataSources);
|
|
1118
1138
|
}
|
|
1119
1139
|
|
|
1140
|
+
if (spotterExperienceVersion !== undefined) {
|
|
1141
|
+
params[Param.SpotterExperienceVersion] = spotterExperienceVersion;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1120
1144
|
// Handle spotterChatConfig params
|
|
1121
1145
|
if (spotterChatConfig) {
|
|
1122
1146
|
const {
|
|
@@ -1281,7 +1305,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1281
1305
|
|
|
1282
1306
|
// Navigation V1/V2 and home page V1/V2 are deprecated. V3 is now the
|
|
1283
1307
|
// default experience, Need to send navigationVersion=v3 and
|
|
1284
|
-
// homepageVersion=v3 even when the no discoveryExperience /
|
|
1308
|
+
// homepageVersion=v3 even when the no discoveryExperience /
|
|
1285
1309
|
// modularHomeExperience config is set. Without these
|
|
1286
1310
|
// params, older TSA versions (< 26.7) fall back to V2 for nav and
|
|
1287
1311
|
// home page, so the defaults must be sent explicitly from the SDK.
|
|
@@ -1293,7 +1317,7 @@ export class AppEmbed extends V1Embed {
|
|
|
1293
1317
|
if (discoveryExperience.listPageVersion !== undefined) {
|
|
1294
1318
|
params[Param.ListPageVersion] = discoveryExperience.listPageVersion;
|
|
1295
1319
|
}
|
|
1296
|
-
|
|
1320
|
+
|
|
1297
1321
|
if (discoveryExperience.homePage === HomePage.Focused) {
|
|
1298
1322
|
params[Param.HomepageVersion] = HomePage.Focused;
|
|
1299
1323
|
// 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
|
});
|