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

thoughtspot / visual-embed-sdk / #1337

03 Jan 2025 06:00AM UTC coverage: 90.769% (-2.9%) from 93.701%
#1337

Pull #65

yinstardev
SCAL-233454-exp temp-testcases skip
Pull Request #65: test-exported memb

918 of 1087 branches covered (84.45%)

Branch coverage included in aggregate %.

76 of 135 new or added lines in 7 files covered. (56.3%)

19 existing lines in 2 files now uncovered.

2376 of 2542 relevant lines covered (93.47%)

52.69 hits per line

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

71.43
/src/native/commonUtils.ts
1
import EventEmitter from 'eventemitter3';
1✔
2
import { AuthEvent, AuthStatus, setAuthEE } from '../auth';
1✔
3
import { setMobileEmbedConfig } from '../embed/embedConfig';
1✔
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';
1✔
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
    const defaultHandleMessage = async () => {
6✔
77
        switch (message.type) {
5✔
78
            case 'appInit': {
79
                try {
2✔
80
                    const authToken = await config.getAuthToken();
2✔
81
                    const initPayload = {
1✔
82
                        type: 'appInit',
83
                        data: {
84
                            host: config.thoughtSpotHost,
85
                            authToken,
86
                            customisations: getCustomisationsMobileEmbed(config),
87
                        },
88
                    };
89
                    injectJavaScript(jsCodeToHandleInteractionsForContextMenu);
1✔
90
                    injectJavaScript(`window.postMessage(${JSON.stringify(initPayload)}, '*');`);
1✔
91
                } catch (error) {
92
                    console.error('Error handling appInit:', error);
1✔
93
                }
94
                break;
1✔
95
            }
96

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

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

129
            default:
130
                console.warn('Unhandled message type:', message.type);
1✔
131
        }
132
    };
133

134
    if (config.handleMessage) {
6✔
135
        await config.handleMessage(event);
1✔
136
    } else {
137
        await defaultHandleMessage();
5✔
138
    }
139
};
140

141
/**
142
 *
143
 * @param embedConfig
144
 * @param webViewRef
145
 */
146
export function initMobile(embedConfig: WebViewConfig, webViewRef?: any)
1✔
147
: EventEmitter<AuthStatus | AuthEvent> {
NEW
148
    const authEE = new EventEmitter<AuthStatus | AuthEvent>();
×
NEW
149
    setMobileEmbedConfig(embedConfig);
×
NEW
150
    setAuthEE(authEE);
×
151

NEW
152
    handleAuth();
×
153

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

NEW
166
    return authEE;
×
167
}
168

169
const jsCodeToHandleInteractionsForContextMenu = `
1✔
170
// Disabling auofocus
171
document.querySelectorAll('input[autofocus], textarea[autofocus]').forEach(el => el.removeAttribute('autofocus'));
172

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

179
// input focus problem. -> we can just force it inside our view. 
180
document.addEventListener('focusin', (event) => {
181
  const target = event.target;
182

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

198
      let scrollX = 0;
199

200
      if (rect.left < horizontalPadding) {
201
        scrollX = rect.left - horizontalPadding;
202
      }  
203
      if (rect.right > window.innerWidth - horizontalPadding) {
204
        scrollX = rect.right - window.innerWidth + horizontalPadding;
205
      }
206
      const scrollY = rect.top - (window.innerHeight / 2 - rect.height / 2);
207

208
      window.scrollBy({
209
        top: scrollY,
210
        left: scrollX,
211
        behavior: 'smooth',
212
      })
213
    }
214
  }
215
});
216
`;
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

© 2025 Coveralls, Inc