Skip to content

Commit 6649d3a

Browse files
SCAL-255929 - Added cleanup event on destroy (#338)
1 parent cb0605f commit 6649d3a

File tree

5 files changed

+207
-46
lines changed

5 files changed

+207
-46
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@thoughtspot/visual-embed-sdk",
3-
"version": "1.42.2",
3+
"version": "1.42.3",
44
"description": "ThoughtSpot Embed SDK",
55
"module": "lib/src/index.js",
66
"main": "dist/tsembed.js",

src/embed/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
4141
authTriggerText: 'Authorize',
4242
authType: AuthType.None,
4343
logLevel: LogLevel.ERROR,
44+
waitForCleanupOnDestroy: false,
45+
cleanupTimeout: 5000,
4446
};
4547

4648
export interface executeTMLInput {

src/embed/ts-embed.spec.ts

Lines changed: 168 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,50 +2428,6 @@ describe('Unit test case for ts embed', () => {
24282428
});
24292429
});
24302430

2431-
describe('When destroyed', () => {
2432-
it('should remove the iframe', async () => {
2433-
const appEmbed = new AppEmbed(getRootEl(), {
2434-
frameParams: {
2435-
width: '100%',
2436-
height: '100%',
2437-
},
2438-
});
2439-
await appEmbed.render();
2440-
expect(getIFrameEl()).toBeTruthy();
2441-
appEmbed.destroy();
2442-
expect(getIFrameEl()).toBeFalsy();
2443-
});
2444-
2445-
it('should remove the iframe when insertAsSibling is true', async () => {
2446-
const appEmbed = new AppEmbed(getRootEl(), {
2447-
frameParams: {
2448-
width: '100%',
2449-
height: '100%',
2450-
},
2451-
insertAsSibling: true,
2452-
});
2453-
await appEmbed.render();
2454-
expect(getIFrameEl()).toBeTruthy();
2455-
appEmbed.destroy();
2456-
expect(getIFrameEl()).toBeFalsy();
2457-
});
2458-
2459-
it("Should remove the error message on destroy if it's present", async () => {
2460-
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(false);
2461-
const appEmbed = new AppEmbed(getRootEl(), {
2462-
frameParams: {
2463-
width: '100%',
2464-
height: '100%',
2465-
},
2466-
insertAsSibling: true,
2467-
});
2468-
await appEmbed.render();
2469-
expect(getRootEl().nextElementSibling.innerHTML).toContain('Not logged in');
2470-
appEmbed.destroy();
2471-
expect(getRootEl().nextElementSibling.innerHTML).toBe('');
2472-
});
2473-
});
2474-
24752431
describe('validate getThoughtSpotPostUrlParams', () => {
24762432
const { location } = window;
24772433

@@ -3456,4 +3412,172 @@ describe('Unit test case for ts embed', () => {
34563412
triggerSpy.mockReset();
34573413
});
34583414
});
3415+
3416+
describe('When destroyed', () => {
3417+
it('should remove the iframe', async () => {
3418+
const appEmbed = new AppEmbed(getRootEl(), {
3419+
frameParams: {
3420+
width: '100%',
3421+
height: '100%',
3422+
},
3423+
});
3424+
await appEmbed.render();
3425+
expect(getIFrameEl()).toBeTruthy();
3426+
appEmbed.destroy();
3427+
expect(getIFrameEl()).toBeFalsy();
3428+
});
3429+
3430+
it('should remove the iframe when insertAsSibling is true', async () => {
3431+
const appEmbed = new AppEmbed(getRootEl(), {
3432+
frameParams: {
3433+
width: '100%',
3434+
height: '100%',
3435+
},
3436+
insertAsSibling: true,
3437+
});
3438+
await appEmbed.render();
3439+
expect(getIFrameEl()).toBeTruthy();
3440+
appEmbed.destroy();
3441+
expect(getIFrameEl()).toBeFalsy();
3442+
});
3443+
3444+
it("Should remove the error message on destroy if it's present", async () => {
3445+
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(false);
3446+
const appEmbed = new AppEmbed(getRootEl(), {
3447+
frameParams: {
3448+
width: '100%',
3449+
height: '100%',
3450+
},
3451+
insertAsSibling: true,
3452+
});
3453+
await appEmbed.render();
3454+
expect(getRootEl().nextElementSibling.innerHTML).toContain('Not logged in');
3455+
appEmbed.destroy();
3456+
expect(getRootEl().nextElementSibling.innerHTML).toBe('');
3457+
});
3458+
3459+
describe('with waitForCleanupOnDestroy configuration', () => {
3460+
let originalEmbedConfig: any;
3461+
3462+
beforeEach(() => {
3463+
originalEmbedConfig = embedConfig.getEmbedConfig();
3464+
});
3465+
3466+
afterEach(() => {
3467+
embedConfig.setEmbedConfig(originalEmbedConfig);
3468+
});
3469+
3470+
it('should trigger DestroyEmbed event immediately when waitForCleanupOnDestroy is false', async () => {
3471+
embedConfig.setEmbedConfig({
3472+
...originalEmbedConfig,
3473+
waitForCleanupOnDestroy: false,
3474+
});
3475+
3476+
const appEmbed = new AppEmbed(getRootEl(), {
3477+
frameParams: {
3478+
width: '100%',
3479+
height: '100%',
3480+
},
3481+
});
3482+
await appEmbed.render();
3483+
3484+
const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockResolvedValue(null);
3485+
const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl());
3486+
3487+
appEmbed.destroy();
3488+
3489+
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.DestroyEmbed);
3490+
expect(removeChildSpy).toHaveBeenCalled();
3491+
});
3492+
3493+
it('should trigger DestroyEmbed event and wait for cleanup when waitForCleanupOnDestroy is true', async () => {
3494+
embedConfig.setEmbedConfig({
3495+
...originalEmbedConfig,
3496+
waitForCleanupOnDestroy: true,
3497+
cleanupTimeout: 1000,
3498+
});
3499+
3500+
const appEmbed = new AppEmbed(getRootEl(), {
3501+
frameParams: {
3502+
width: '100%',
3503+
height: '100%',
3504+
},
3505+
});
3506+
await appEmbed.render();
3507+
3508+
const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockResolvedValue(null);
3509+
const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl());
3510+
3511+
appEmbed.destroy();
3512+
3513+
// Should be called immediately when waitForCleanupOnDestroy is true
3514+
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.DestroyEmbed);
3515+
3516+
// Wait for the timeout to complete
3517+
await new Promise(resolve => setTimeout(resolve, 1100));
3518+
3519+
expect(removeChildSpy).toHaveBeenCalled();
3520+
});
3521+
3522+
it('should handle Promise.race with successful cleanup completion', async () => {
3523+
embedConfig.setEmbedConfig({
3524+
...originalEmbedConfig,
3525+
waitForCleanupOnDestroy: true,
3526+
cleanupTimeout: 2000,
3527+
});
3528+
3529+
const appEmbed = new AppEmbed(getRootEl(), {
3530+
frameParams: {
3531+
width: '100%',
3532+
height: '100%',
3533+
},
3534+
});
3535+
await appEmbed.render();
3536+
3537+
// Mock trigger to resolve quickly (before timeout)
3538+
const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() =>
3539+
new Promise(resolve => setTimeout(() => resolve(null), 100))
3540+
);
3541+
const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl());
3542+
3543+
appEmbed.destroy();
3544+
3545+
// Wait for the trigger to complete
3546+
await new Promise(resolve => setTimeout(resolve, 200));
3547+
3548+
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.DestroyEmbed);
3549+
expect(removeChildSpy).toHaveBeenCalled();
3550+
});
3551+
3552+
it('should handle Promise.race with timeout when cleanup takes too long', async () => {
3553+
embedConfig.setEmbedConfig({
3554+
...originalEmbedConfig,
3555+
waitForCleanupOnDestroy: true,
3556+
cleanupTimeout: 100,
3557+
});
3558+
3559+
const appEmbed = new AppEmbed(getRootEl(), {
3560+
frameParams: {
3561+
width: '100%',
3562+
height: '100%',
3563+
},
3564+
});
3565+
await appEmbed.render();
3566+
3567+
// Mock trigger to take longer than timeout
3568+
const triggerSpy = jest.spyOn(appEmbed, 'trigger').mockImplementation(() =>
3569+
new Promise(resolve => setTimeout(() => resolve(null), 500))
3570+
);
3571+
const removeChildSpy = jest.spyOn(Node.prototype, 'removeChild').mockImplementation(() => getRootEl());
3572+
3573+
appEmbed.destroy();
3574+
3575+
// Wait for the timeout to complete
3576+
await new Promise(resolve => setTimeout(resolve, 200));
3577+
3578+
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.DestroyEmbed);
3579+
expect(removeChildSpy).toHaveBeenCalled();
3580+
});
3581+
});
3582+
});
34593583
});

src/embed/ts-embed.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1396,8 +1396,21 @@ export class TsEmbed {
13961396
public destroy(): void {
13971397
try {
13981398
this.removeFullscreenChangeHandler();
1399-
this.insertedDomEl?.parentNode.removeChild(this.insertedDomEl);
14001399
this.unsubscribeToEvents();
1400+
if (!getEmbedConfig().waitForCleanupOnDestroy) {
1401+
this.trigger(HostEvent.DestroyEmbed)
1402+
this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
1403+
} else {
1404+
const cleanupTimeout = getEmbedConfig().cleanupTimeout;
1405+
Promise.race([
1406+
this.trigger(HostEvent.DestroyEmbed),
1407+
new Promise((resolve) => setTimeout(resolve, cleanupTimeout)),
1408+
]).then(() => {
1409+
this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
1410+
}).catch((e) => {
1411+
logger.log('Error destroying TS Embed', e);
1412+
});
1413+
}
14011414
} catch (e) {
14021415
logger.log('Error destroying TS Embed', e);
14031416
}

src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,19 @@ export interface EmbedConfig {
692692
* ```
693693
*/
694694
customActions?: CustomAction[];
695+
696+
/**
697+
* Wait for the cleanup to be completed before destroying the embed.
698+
* @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl
699+
* @default false
700+
*/
701+
waitForCleanupOnDestroy?: boolean;
702+
/**
703+
* The timeout for the cleanup to be completed before destroying the embed.
704+
* @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl
705+
* @default 5000
706+
*/
707+
cleanupTimeout?: number;
695708
}
696709

697710
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
@@ -4247,6 +4260,15 @@ export enum HostEvent {
42474260
* ```
42484261
*/
42494262
UpdateEmbedParams = 'updateEmbedParams',
4263+
/**
4264+
* Triggered when the embed is needed to be destroyed. This is used to clean up any embed related resources internally.
4265+
* @example
4266+
* ```js
4267+
* liveboardEmbed.trigger(HostEvent.DestroyEmbed);
4268+
* ```
4269+
* @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl
4270+
*/
4271+
DestroyEmbed = 'EmbedDestroyed',
42504272
}
42514273

42524274
/**

0 commit comments

Comments
 (0)