• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / visual-embed-sdk / #1451

16 Jan 2025 06:06AM UTC coverage: 41.695% (-52.2%) from 93.86%
#1451

Pull #65

Prashant.patil
SCAL-233454-exp removing all extra config and checking
Pull Request #65: test-exported memb

273 of 1114 branches covered (24.51%)

Branch coverage included in aggregate %.

84 of 366 new or added lines in 14 files covered. (22.95%)

1011 existing lines in 23 files now uncovered.

1301 of 2661 relevant lines covered (48.89%)

9.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.86
/src/native/commonUtils.ts
1
import EventEmitter from 'eventemitter3';
2
import { AuthEvent, AuthStatus, setAuthEE } from '../auth';
3
import { setMobileEmbedConfig } from '../embed/embedConfig';
4
import { getCustomisationsMobileEmbed, getQueryParamString } from '../utils';
1✔
5
import { Param } from '../types';
1✔
6
import pkgInfo from '../../package.json';
1✔
7
import { WebViewConfig } from './types';
8
import { logger } from '../utils/logger';
1✔
9
import { handleAuth } from './handleAuth';
10

11
/**
12
 * This method constructs the webview URL with given config.
13
 * @param config To get the webviewURL pass the necessary config options.
14
 * host: string;
15
 * authType: AuthType;
16
 * liveboardId: string;
17
 * getAuthToken: () => Promise<string>;
18
 * These four are necessary arguments.
19
 * @returns The Promise for WebView URL.
20
 */
21
export const getWebViewUrl = async (config: WebViewConfig): Promise<string> => {
1✔
22
    if (typeof config.getAuthToken !== 'function') {
4✔
23
        throw new Error('`getAuthToken` must be a function that returns a Promise.');
1✔
24
    }
25

26
    const authToken = await config.getAuthToken();
3✔
27
    if (!authToken) {
3✔
28
        throw new Error('Failed to fetch initial authentication token.');
1✔
29
    }
30

31
    const hostAppUrl = encodeURIComponent(
2✔
32
        config.thoughtSpotHost.includes('localhost')
6✔
33
      || config.thoughtSpotHost.includes('127.0.0.1')
34
      || config.thoughtSpotHost.includes('10.0.2.2')
35
            ? 'local-host'
36
            : config.thoughtSpotHost,
37
    );
38

39
    const queryParams = {
2✔
40
        [Param.EmbedApp]: true,
41
        [Param.HostAppUrl]: hostAppUrl,
42
        [Param.Version]: pkgInfo.version,
43
        [Param.AuthType]: config.authType,
44
        [Param.livedBoardEmbed]: true,
45
        [Param.EnableFlipTooltipToContextMenu]: true,
46
        [Param.ContextMenuTrigger]: true,
47
    };
48

49
    const queryString = getQueryParamString(queryParams);
2✔
50
    const webViewUrl = `${config.thoughtSpotHost}/embed?${queryString}#/embed/viz/${encodeURIComponent(config.liveboardId)}`;
2✔
51
    return webViewUrl;
2✔
52
};
53

54
/**
55
 * setting up message handling for the message replies to TS instances.
56
 * @param config The webview config
57
 * @param event The message event from the WebView.
58
 * @param WebViewRef Ref to use and inject javascript
59
 * @param webViewRef
60
 */
61
export const setupWebViewMessageHandler = async (
1✔
62
    config: WebViewConfig,
63
    event: any,
64
    webViewRef: any,
65
) => {
66
    const message = JSON.parse(event.nativeEvent.data);
6✔
67

68
    const injectJavaScript = (codeSnip: string) => {
6✔
69
        if (webViewRef?.current) {
4!
70
            webViewRef.current.injectJavaScript(codeSnip);
4✔
71
        } else {
NEW
72
            logger.error('Reference for Webview not found!!');
×
73
        }
74
    };
75

76
    // scaligent -> some page -> sending some data to scaligent
77
    // static page -> we have send to scaligent ->
78
    // iframe -> .onMessage(()) ->
79

80
    const defaultHandleMessage = async () => {
6✔
81
        switch (message.type) {
5✔
82
            case 'appInit': {
83
                try {
2✔
84
                    const authToken = await config.getAuthToken();
2✔
85
                    const initPayload = {
1✔
86
                        type: 'appInit',
87
                        data: {
88
                            host: config.thoughtSpotHost,
89
                            authToken,
90
                            customisations: getCustomisationsMobileEmbed(config),
91
                        },
92
                    };
93
                    injectJavaScript(jsCodeToHandleInteractionsForContextMenu);
1✔
94
                    injectJavaScript(`window.postMessage(${JSON.stringify(initPayload)}, '*');`);
1✔
95
                } catch (error) {
96
                    console.error('Error handling appInit:', error);
1✔
97
                }
98
                break;
1✔
99
            }
100

101
            case 'ThoughtspotAuthExpired': {
102
                try {
1✔
103
                    const newAuthToken = await config.getAuthToken();
1✔
104
                    if (newAuthToken) {
1✔
105
                        const authExpirePayload = {
1✔
106
                            type: 'ThoughtspotAuthExpired',
107
                            data: { authToken: newAuthToken },
108
                        };
109
                        injectJavaScript(`window.postMessage(${JSON.stringify(authExpirePayload)}, '*');`);
1✔
110
                    }
111
                } catch (error) {
NEW
112
                    console.error('Error refreshing token on expiry:', error);
×
113
                }
114
                break;
1✔
115
            }
116

117
            case 'ThoughtspotAuthFailure': {
118
                try {
1✔
119
                    const newAuthToken = await config.getAuthToken();
1✔
120
                    if (newAuthToken) {
1✔
121
                        const authFailurePayload = {
1✔
122
                            type: 'ThoughtspotAuthFailure',
123
                            data: { authToken: newAuthToken },
124
                        };
125
                        injectJavaScript(`window.postMessage(${JSON.stringify(authFailurePayload)}, '*');`);
1✔
126
                    }
127
                } catch (error) {
NEW
128
                    console.error('Error refreshing token on failure:', error);
×
129
                }
130
                break;
1✔
131
            }
132

133
            default:
134
                console.warn('Unhandled message type:', message.type);
1✔
135
        }
136
    };
137

138
    if (config.handleMessage) {
6✔
139
        await config.handleMessage(event);
1✔
140
    } else {
141
        await defaultHandleMessage();
5✔
142
    }
143
};
144

145
// /**
146
//  *
147
//  * @param embedConfig
148
//  * @param webViewRef
149
//  */
150
// export function initMobile(embedConfig: WebViewConfig, webViewRef?: any)
151
// : EventEmitter<AuthStatus | AuthEvent> {
152
//     const authEE = new EventEmitter<AuthStatus | AuthEvent>();
153
//     setMobileEmbedConfig(embedConfig);
154
//     setAuthEE(authEE);
155

156
//     handleAuth();
157

158
//     if (embedConfig.autoAttachWebViewHandler && webViewRef?.current) {
159
//         const originalOnMessage = webViewRef.current.props?.onMessage;
160
//         webViewRef.current.props.onMessage = (event: any) => {
161
//             // If the user has some onMessage Added.
162
//             if (originalOnMessage) {
163
//                 originalOnMessage(event);
164
//             }
165
//             // Then we execute ours.
166
//             setupWebViewMessageHandler(embedConfig, event, webViewRef);
167
//         };
168
//     }
169

170
//     return authEE;
171
// }
172

173
const jsCodeToHandleInteractionsForContextMenu = `
1✔
174
// Disabling auofocus
175
document.querySelectorAll('input[autofocus], textarea[autofocus]').forEach(el => el.removeAttribute('autofocus'));
176

177
// adding meta tag to keep fixed viewport scalign
178
const meta = document.createElement('meta');
179
meta.name = 'viewport';
180
meta.content = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';
181
document.head.appendChild(meta);
182

183
// input focus problem. -> we can just force it inside our view. 
184
document.addEventListener('focusin', (event) => {
185
  const target = event.target;
186

187
  if (
188
    target.tagName === 'INPUT' || 
189
    target.tagName === 'TEXTAREA'
190
  ) {
191
    const rect = target.getBoundingClientRect();
192
    if (
193
      rect.top < 0 || 
194
      rect.bottom > window.innerHeight || 
195
      rect.left < 0 || 
196
      rect.right > window.innerWidth
197
    ) {
198
      event.preventDefault();
199
      // target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'end' });
200
      const horizontalPadding = 10;
201

202
      let scrollX = 0;
203

204
      if (rect.left < horizontalPadding) {
205
        scrollX = rect.left - horizontalPadding;
206
      }  
207
      if (rect.right > window.innerWidth - horizontalPadding) {
208
        scrollX = rect.right - window.innerWidth + horizontalPadding;
209
      }
210
      const scrollY = rect.top - (window.innerHeight / 2 - rect.height / 2);
211

212
      window.scrollBy({
213
        top: scrollY,
214
        left: scrollX,
215
        behavior: 'smooth',
216
      })
217
    }
218
  }
219
});
220
`;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc