Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/embed/ts-embed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3345,4 +3345,115 @@ describe('Unit test case for ts embed', () => {
expect(searchEmbed.isEmbedContainerLoaded).toBe(true);
});
});

describe('Online event listener registration after auth failure', () => {
beforeAll(() => {
init({
thoughtSpotHost: 'tshost',
authType: AuthType.None,
loginFailedMessage: 'Not logged in',
});
});

test('should register online event listener when authentication fails', async () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
new Error('Auth failed'),
);
const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
addEventListenerSpy.mockClear();
await searchEmbed.render();
await executeAfterWait(() => {
expect(getRootEl().innerHTML).toContain('Not logged in');
const onlineListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'online',
);
expect(onlineListenerCalls).toHaveLength(1);
const offlineListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'offline',
);
expect(offlineListenerCalls).toHaveLength(1);
const messageListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'message',
);
expect(messageListenerCalls).toHaveLength(0);
});

addEventListenerSpy.mockRestore();
});

test('should attempt to trigger reload when online event occurs after auth failure', async () => {
jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
new Error('Auth failed'),
);
const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
const triggerSpy = jest.spyOn(searchEmbed, 'trigger').mockResolvedValue(null);
await searchEmbed.render();

await executeAfterWait(() => {
expect(getRootEl().innerHTML).toContain('Not logged in');
triggerSpy.mockClear();
const onlineEvent = new Event('online');
window.dispatchEvent(onlineEvent);
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.Reload);
});

triggerSpy.mockReset();
});

test('should handle online event gracefully when no iframe exists', async () => {
jest.spyOn(baseInstance, 'getAuthPromise').mockRejectedValueOnce(
new Error('Auth failed'),
);
const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
await searchEmbed.render();
await executeAfterWait(() => {
expect(getRootEl().innerHTML).toContain('Not logged in');
const onlineEvent = new Event('online');
expect(() => {
window.dispatchEvent(onlineEvent);
}).not.toThrow();
});

errorSpy.mockReset();
});

test('should register all event listeners when authentication succeeds', async () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(true);
const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
addEventListenerSpy.mockClear();
await searchEmbed.render();
await executeAfterWait(() => {
const onlineListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'online',
);
expect(onlineListenerCalls).toHaveLength(1);
const offlineListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'offline',
);
expect(offlineListenerCalls).toHaveLength(1);
const messageListenerCalls = addEventListenerSpy.mock.calls.filter(
(call) => call[0] === 'message',
);
expect(messageListenerCalls).toHaveLength(1);
});

addEventListenerSpy.mockRestore();
});
test('should successfully trigger reload when online event occurs after auth success', async () => {
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(true);
const searchEmbed = new SearchEmbed(getRootEl(), defaultViewConfig);
const triggerSpy = jest.spyOn(searchEmbed, 'trigger').mockResolvedValue({} as any);
await searchEmbed.render();
await executeAfterWait(() => {
triggerSpy.mockClear();
const onlineEvent = new Event('online');
window.dispatchEvent(onlineEvent);
expect(triggerSpy).toHaveBeenCalledWith(HostEvent.Reload);
});
triggerSpy.mockReset();
});
});
});
98 changes: 74 additions & 24 deletions src/embed/ts-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,36 @@ export class TsEmbed {
private subscribedListeners: Record<string, any> = {};

/**
* Adds a global event listener to window for "message" events.
* ThoughtSpot detects if a particular event is targeted to this
* embed instance through an identifier contained in the payload,
* and executes the registered callbacks accordingly.
* Subscribe to network events (online/offline) that should
* work regardless of auth status
*/
private subscribeToEvents() {
this.unsubscribeToEvents();
private subscribeToNetworkEvents() {
this.unsubscribeToNetworkEvents();

const onlineEventListener = (e: Event) => {
this.trigger(HostEvent.Reload);
};
window.addEventListener('online', onlineEventListener);

const offlineEventListener = (e: Event) => {
const offlineWarning = ERROR_MESSAGE.OFFLINE_WARNING;
this.executeCallbacks(EmbedEvent.Error, {
offlineWarning,
});
logger.warn(offlineWarning);
};
window.addEventListener('offline', offlineEventListener);

this.subscribedListeners.online = onlineEventListener;
this.subscribedListeners.offline = offlineEventListener;
}

/**
* Subscribe to message events that depend on successful iframe setup
*/
private subscribeToMessageEvents() {
this.unsubscribeToMessageEvents();

const messageEventListener = (event: MessageEvent<any>) => {
const eventType = this.getEventType(event);
const eventPort = this.getEventPort(event);
Expand All @@ -338,25 +361,37 @@ export class TsEmbed {
};
window.addEventListener('message', messageEventListener);

const onlineEventListener = (e: Event) => {
this.trigger(HostEvent.Reload);
};
window.addEventListener('online', onlineEventListener);
this.subscribedListeners.message = messageEventListener;
}
/**
* Adds event listeners for both network and message events.
* This maintains backward compatibility with the existing method.
* Adds a global event listener to window for "message" events.
* ThoughtSpot detects if a particular event is targeted to this
* embed instance through an identifier contained in the payload,
* and executes the registered callbacks accordingly.
*/
private subscribeToEvents() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this if we are calling network events function and message events function seperately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling From showPreRender function

this.subscribeToNetworkEvents();
this.subscribeToMessageEvents();
}

const offlineEventListener = (e: Event) => {
const offlineWarning = 'Network not Detected. Embed is offline. Please reconnect and refresh';
this.executeCallbacks(EmbedEvent.Error, {
offlineWarning,
});
logger.warn(offlineWarning);
};
window.addEventListener('offline', offlineEventListener);
private unsubscribeToNetworkEvents() {
if (this.subscribedListeners.online) {
window.removeEventListener('online', this.subscribedListeners.online);
delete this.subscribedListeners.online;
}
if (this.subscribedListeners.offline) {
window.removeEventListener('offline', this.subscribedListeners.offline);
delete this.subscribedListeners.offline;
}
}

this.subscribedListeners = {
message: messageEventListener,
online: onlineEventListener,
offline: offlineEventListener,
};
private unsubscribeToMessageEvents() {
if (this.subscribedListeners.message) {
window.removeEventListener('message', this.subscribedListeners.message);
delete this.subscribedListeners.message;
}
}

private unsubscribeToEvents() {
Expand Down Expand Up @@ -787,6 +822,9 @@ export class TsEmbed {

uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);

// Always subscribe to network events, regardless of auth status
this.subscribeToNetworkEvents();

return getAuthPromise()
?.then((isLoggedIn: boolean) => {
if (!isLoggedIn) {
Expand Down Expand Up @@ -832,7 +870,9 @@ export class TsEmbed {
el.remove();
});
}
this.subscribeToEvents();
// Subscribe to message events only after successful
// auth and iframe setup
this.subscribeToMessageEvents();
})
.catch((error) => {
nextInQueue();
Expand Down Expand Up @@ -1232,6 +1272,16 @@ export class TsEmbed {
this.handleError('Host event type is undefined');
return null;
}

// Check if iframe exists before triggering -
// this prevents the error when auth fails
if (!this.iFrame) {
logger.debug(
`Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
);
return null;
}

// send an empty object, this is needed for liveboard default handlers
return this.hostEventClient.triggerHostEvent(messageType, data);
}
Expand Down
1 change: 1 addition & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const ERROR_MESSAGE = {
MISSING_REPORTING_OBSERVER: 'ReportingObserver not supported',
RENDER_CALLED_BEFORE_INIT: 'Looks like render was called before calling init, the render won\'t start until init is called.\nFor more info check\n1. https://developers.thoughtspot.com/docs/Function_init#_init\n2.https://developers.thoughtspot.com/docs/getting-started#initSdk',
SPOTTER_AGENT_NOT_INITIALIZED: 'SpotterAgent not initialized',
OFFLINE_WARNING : 'Network not Detected. Embed is offline. Please reconnect and refresh',
};

export const CUSTOM_ACTIONS_ERROR_MESSAGE = {
Expand Down
Loading