diff --git a/src/embed/app.spec.ts b/src/embed/app.spec.ts index 954785fb..d4ccfa62 100644 --- a/src/embed/app.spec.ts +++ b/src/embed/app.spec.ts @@ -1709,6 +1709,116 @@ describe('App embed tests', () => { 'Please call render before invoking this method', ); }); + + describe('overrideHistoryState flag', () => { + test('navigateToPage with overrideHistoryState=true should trigger HostEvent.Navigate', async () => { + mockMessageChannel(); + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: true, + }); + await appEmbed.render(); + + const iframe = getIFrameEl(); + iframe.contentWindow.postMessage = jest.fn(); + appEmbed.navigateToPage(path, false); + + expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: HostEvent.Navigate, + data: path, + }), + `http://${thoughtSpotHost}`, + expect.anything(), + ); + }); + + test('navigateToPage with overrideHistoryState=true and path as number should trigger HostEvent.Navigate', async () => { + mockMessageChannel(); + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: true, + }); + await appEmbed.render(); + + const iframe = getIFrameEl(); + iframe.contentWindow.postMessage = jest.fn(); + appEmbed.navigateToPage(-1, false); + + expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: HostEvent.Navigate, + data: -1, + }), + `http://${thoughtSpotHost}`, + expect.anything(), + ); + }); + + test('navigateToPage with overrideHistoryState=false and noReload=false should update iframe src', async () => { + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: false, + }); + await appEmbed.render(); + appEmbed.navigateToPage(path, false); + + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/?embedApp=true&primaryNavHidden=true&profileAndHelpInNavBarHidden=false&${defaultParamsForPinboardEmbed}${defaultParamsPost}#/${path}`, + ); + }); + + test('navigateToPage with overrideHistoryState=undefined and noReload=false should update iframe src', async () => { + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + }); + await appEmbed.render(); + appEmbed.navigateToPage(path, false); + + expectUrlMatchesWithParams( + getIFrameSrc(), + `http://${thoughtSpotHost}/?embedApp=true&primaryNavHidden=true&profileAndHelpInNavBarHidden=false&${defaultParamsForPinboardEmbed}${defaultParamsPost}#/${path}`, + ); + }); + + test('navigateToPage respects noReload priority over overrideHistoryState', async () => { + mockMessageChannel(); + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: false, + }); + await appEmbed.render(); + + const iframe = getIFrameEl(); + iframe.contentWindow.postMessage = jest.fn(); + appEmbed.navigateToPage(path, true); + + expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: HostEvent.Navigate, + data: path, + }), + `http://${thoughtSpotHost}`, + expect.anything(), + ); + }); + }); }); describe('LazyLoadingForFullHeight functionality', () => { diff --git a/src/embed/app.ts b/src/embed/app.ts index b68e0fb1..5f2715cb 100644 --- a/src/embed/app.ts +++ b/src/embed/app.ts @@ -1335,7 +1335,8 @@ export class AppEmbed extends V1Embed { logger.log('Please call render before invoking this method'); return; } - if (noReload) { + const overrideHistoryState = this.viewConfig?.overrideHistoryState; + if (noReload || overrideHistoryState) { this.trigger(HostEvent.Navigate, path); } else { if (typeof path !== 'string') { diff --git a/src/embed/ts-embed.spec.ts b/src/embed/ts-embed.spec.ts index 63625024..72df141d 100644 --- a/src/embed/ts-embed.spec.ts +++ b/src/embed/ts-embed.spec.ts @@ -2281,6 +2281,46 @@ describe('Unit test case for ts embed', () => { }); }); + it('Sets the overrideHistoryState param', async () => { + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: true, + }); + await appEmbed.render(); + expectUrlToHaveParamsWithValues(getIFrameSrc(), { + overrideHistoryState: true, + }); + }); + + it('Sets the overrideHistoryState param to false', async () => { + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + overrideHistoryState: false, + }); + await appEmbed.render(); + expectUrlToHaveParamsWithValues(getIFrameSrc(), { + overrideHistoryState: false, + }); + }); + + it('Should not add overrideHistoryState param when it is undefined', async () => { + const appEmbed = new AppEmbed(getRootEl(), { + frameParams: { + width: '100%', + height: '100%', + }, + }); + await appEmbed.render(); + const url = getIFrameSrc(); + expect(url).not.toContain('overrideHistoryState'); + }); + it('Should not add contextMenuEnabledOnWhichClick flag to the iframe src when it is not passed', async () => { const liveboardEmbed = new LiveboardEmbed(getRootEl(), { ...defaultViewConfig, diff --git a/src/embed/ts-embed.ts b/src/embed/ts-embed.ts index 96d834a2..b1fc0ac1 100644 --- a/src/embed/ts-embed.ts +++ b/src/embed/ts-embed.ts @@ -758,6 +758,7 @@ export class TsEmbed { insertInToSlide, disableRedirectionLinksInNewTab, overrideOrgId, + overrideHistoryState, exposeTranslationIDs, primaryAction, } = this.viewConfig; @@ -856,6 +857,9 @@ export class TsEmbed { if (overrideOrgId !== undefined) { queryParams[Param.OverrideOrgId] = overrideOrgId; } + if (overrideHistoryState !== undefined) { + queryParams[Param.OverrideHistoryState] = overrideHistoryState; + } if (this.isPreAuthCacheEnabled()) { queryParams[Param.preAuthCache] = true; diff --git a/src/types.ts b/src/types.ts index 4400136f..7991493d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1179,6 +1179,26 @@ export interface BaseViewConfig extends ApiInterceptFlags { * ``` */ overrideOrgId?: number; + /** + * Overrides the browser history behavior for embedding application users. + * This parameter changes standard history navigation (pushState) into a + * state replacement (replaceState), preventing users from getting trapped in + * back-button loops inside the embedded iframe environment. + * The overrideHistoryState setting is honored only if the + * application is running within an embedded context. + * + * Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed` + * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl + * @example + * ```js + * // Replace with embed component name. For example, AppEmbed, SearchEmbed, or LiveboardEmbed + * const embed = new ('#tsEmbed', { + * // ... other embed view config + * overrideHistoryState: true, + * }); + * ``` + */ + overrideHistoryState?: boolean; /** * Flag to override the *Open Link in New Tab* context * menu option. @@ -6111,6 +6131,7 @@ export enum Param { SpotterEnabled = 'isSpotterExperienceEnabled', IsUnifiedSearchExperienceEnabled = 'isUnifiedSearchExperienceEnabled', OverrideOrgId = 'orgId', + OverrideHistoryState = 'overrideHistoryState', OauthPollingInterval = 'oAuthPollingInterval', IsForceRedirect = 'isForceRedirect', DataSourceId = 'dataSourceId',