@thoughtspot/visual-embed-sdk 1.53.0 → 1.53.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/package.json +1 -1
- package/cjs/src/auth.d.ts.map +1 -1
- package/cjs/src/auth.js +25 -6
- package/cjs/src/auth.js.map +1 -1
- package/cjs/src/auth.spec.js +40 -8
- package/cjs/src/auth.spec.js.map +1 -1
- package/cjs/src/contracts/contracts.spec.js +169 -2
- package/cjs/src/contracts/contracts.spec.js.map +1 -1
- package/cjs/src/contracts/embed-event-payloads.d.ts +212 -14
- package/cjs/src/contracts/embed-event-payloads.d.ts.map +1 -1
- package/cjs/src/contracts/host-event-contracts.d.ts +11 -0
- package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/cjs/src/css-variables.d.ts +140 -0
- package/cjs/src/css-variables.d.ts.map +1 -1
- package/cjs/src/embed/app.d.ts +1 -0
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.d.ts +14 -1
- package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.js +91 -2
- package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.spec.js +112 -7
- package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/cjs/src/embed/conversation.d.ts +14 -0
- package/cjs/src/embed/conversation.d.ts.map +1 -1
- package/cjs/src/embed/conversation.js.map +1 -1
- package/cjs/src/embed/conversation.spec.js +22 -0
- package/cjs/src/embed/conversation.spec.js.map +1 -1
- package/cjs/src/embed/host-events.spec.js +50 -0
- package/cjs/src/embed/host-events.spec.js.map +1 -1
- package/cjs/src/embed/liveboard.d.ts +2 -16
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +29 -10
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +139 -0
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/spotter-utils.d.ts +1 -0
- package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
- package/cjs/src/embed/spotter-utils.js +7 -1
- package/cjs/src/embed/spotter-utils.js.map +1 -1
- package/cjs/src/embed/spotter-utils.spec.js +19 -0
- package/cjs/src/embed/spotter-utils.spec.js.map +1 -1
- package/cjs/src/embed/ts-embed.d.ts +21 -16
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +77 -63
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +240 -7
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +2 -2
- package/cjs/src/index.d.ts.map +1 -1
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/react/all-types-export.d.ts +1 -1
- package/cjs/src/react/all-types-export.d.ts.map +1 -1
- package/cjs/src/react/all-types-export.js.map +1 -1
- package/cjs/src/react/util.d.ts +8 -1
- package/cjs/src/react/util.d.ts.map +1 -1
- package/cjs/src/react/util.js.map +1 -1
- package/cjs/src/types.d.ts +249 -2
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +247 -0
- package/cjs/src/types.js.map +1 -1
- package/cjs/src/utils/runtimeTelemetry.d.ts +11 -0
- package/cjs/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.js +34 -0
- package/cjs/src/utils/runtimeTelemetry.js.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/cjs/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/cjs/src/utils/runtimeTelemetry.spec.js +124 -0
- package/cjs/src/utils/runtimeTelemetry.spec.js.map +1 -0
- package/dist/{index-CyDR8j_0.js → index-DQ7r5f1z.js} +1 -1
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/contracts/embed-event-payloads.d.ts +212 -14
- package/dist/src/contracts/embed-event-payloads.d.ts.map +1 -1
- package/dist/src/contracts/host-event-contracts.d.ts +11 -0
- package/dist/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/dist/src/css-variables.d.ts +140 -0
- package/dist/src/css-variables.d.ts.map +1 -1
- package/dist/src/embed/app.d.ts +1 -0
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/auto-frame-renderer.d.ts +14 -1
- package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/dist/src/embed/conversation.d.ts +14 -0
- package/dist/src/embed/conversation.d.ts.map +1 -1
- package/dist/src/embed/liveboard.d.ts +2 -16
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/spotter-utils.d.ts +1 -0
- package/dist/src/embed/spotter-utils.d.ts.map +1 -1
- package/dist/src/embed/ts-embed.d.ts +21 -16
- package/dist/src/embed/ts-embed.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/react/all-types-export.d.ts +1 -1
- package/dist/src/react/all-types-export.d.ts.map +1 -1
- package/dist/src/react/util.d.ts +8 -1
- package/dist/src/react/util.d.ts.map +1 -1
- package/dist/src/types.d.ts +249 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/runtimeTelemetry.d.ts +11 -0
- package/dist/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/dist/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/dist/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/dist/tsembed-react.es.js +474 -84
- package/dist/tsembed-react.js +473 -83
- package/dist/tsembed.es.js +542 -85
- package/dist/tsembed.js +541 -84
- package/dist/visual-embed-sdk-react-full.d.ts +649 -30
- package/dist/visual-embed-sdk-react.d.ts +646 -28
- package/dist/visual-embed-sdk.d.ts +642 -44
- package/lib/package.json +1 -1
- package/lib/src/auth.d.ts.map +1 -1
- package/lib/src/auth.js +25 -6
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +40 -8
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/contracts/contracts.spec.js +169 -2
- package/lib/src/contracts/contracts.spec.js.map +1 -1
- package/lib/src/contracts/embed-event-payloads.d.ts +212 -14
- package/lib/src/contracts/embed-event-payloads.d.ts.map +1 -1
- package/lib/src/contracts/host-event-contracts.d.ts +11 -0
- package/lib/src/contracts/host-event-contracts.d.ts.map +1 -1
- package/lib/src/css-variables.d.ts +140 -0
- package/lib/src/css-variables.d.ts.map +1 -1
- package/lib/src/embed/app.d.ts +1 -0
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.d.ts +14 -1
- package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -1
- package/lib/src/embed/auto-frame-renderer.js +91 -2
- package/lib/src/embed/auto-frame-renderer.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.spec.js +113 -8
- package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
- package/lib/src/embed/conversation.d.ts +14 -0
- package/lib/src/embed/conversation.d.ts.map +1 -1
- package/lib/src/embed/conversation.js.map +1 -1
- package/lib/src/embed/conversation.spec.js +22 -0
- package/lib/src/embed/conversation.spec.js.map +1 -1
- package/lib/src/embed/host-events.spec.js +50 -0
- package/lib/src/embed/host-events.spec.js.map +1 -1
- package/lib/src/embed/liveboard.d.ts +2 -16
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +28 -10
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +139 -0
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/spotter-utils.d.ts +1 -0
- package/lib/src/embed/spotter-utils.d.ts.map +1 -1
- package/lib/src/embed/spotter-utils.js +7 -1
- package/lib/src/embed/spotter-utils.js.map +1 -1
- package/lib/src/embed/spotter-utils.spec.js +19 -0
- package/lib/src/embed/spotter-utils.spec.js.map +1 -1
- package/lib/src/embed/ts-embed.d.ts +21 -16
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +78 -64
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +240 -7
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +2 -2
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/react/all-types-export.d.ts +1 -1
- package/lib/src/react/all-types-export.d.ts.map +1 -1
- package/lib/src/react/all-types-export.js.map +1 -1
- package/lib/src/react/util.d.ts +8 -1
- package/lib/src/react/util.d.ts.map +1 -1
- package/lib/src/react/util.js.map +1 -1
- package/lib/src/types.d.ts +249 -2
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +247 -0
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/runtimeTelemetry.d.ts +11 -0
- package/lib/src/utils/runtimeTelemetry.d.ts.map +1 -0
- package/lib/src/utils/runtimeTelemetry.js +28 -0
- package/lib/src/utils/runtimeTelemetry.js.map +1 -0
- package/lib/src/utils/runtimeTelemetry.spec.d.ts +2 -0
- package/lib/src/utils/runtimeTelemetry.spec.d.ts.map +1 -0
- package/lib/src/utils/runtimeTelemetry.spec.js +122 -0
- package/lib/src/utils/runtimeTelemetry.spec.js.map +1 -0
- package/lib/src/visual-embed-sdk.d.ts +642 -44
- package/package.json +1 -1
- package/src/auth.spec.ts +47 -11
- package/src/auth.ts +35 -6
- package/src/contracts/contracts.spec.ts +186 -3
- package/src/contracts/embed-event-payloads.ts +281 -14
- package/src/contracts/host-event-contracts.ts +14 -0
- package/src/css-variables.ts +168 -0
- package/src/embed/app.ts +1 -0
- package/src/embed/auto-frame-renderer.spec.ts +134 -8
- package/src/embed/auto-frame-renderer.ts +103 -2
- package/src/embed/conversation.spec.ts +24 -0
- package/src/embed/conversation.ts +14 -0
- package/src/embed/host-events.spec.ts +68 -0
- package/src/embed/liveboard.spec.ts +156 -0
- package/src/embed/liveboard.ts +34 -10
- package/src/embed/spotter-utils.spec.ts +20 -0
- package/src/embed/spotter-utils.ts +12 -1
- package/src/embed/ts-embed.spec.ts +297 -8
- package/src/embed/ts-embed.ts +79 -64
- package/src/index.ts +2 -0
- package/src/react/all-types-export.ts +1 -0
- package/src/react/util.ts +13 -1
- package/src/types.ts +249 -0
- package/src/utils/runtimeTelemetry.spec.ts +148 -0
- package/src/utils/runtimeTelemetry.ts +50 -0
package/package.json
CHANGED
package/src/auth.spec.ts
CHANGED
|
@@ -475,35 +475,48 @@ describe('Unit test for auth', () => {
|
|
|
475
475
|
expect(tokenAuthService.isActiveService).toHaveBeenCalledTimes(1);
|
|
476
476
|
});
|
|
477
477
|
|
|
478
|
-
|
|
479
|
-
Object.defineProperty(window, 'location', {
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
const startSamlPopupFlow = async () => {
|
|
479
|
+
Object.defineProperty(window, 'location', {
|
|
480
|
+
value: { href: '', hash: '', origin: 'https://host.app.com' },
|
|
481
|
+
});
|
|
482
|
+
// samlAuthWindow is module state: retire a popup left open by an
|
|
483
|
+
// earlier test so this flow opens (and trusts) a fresh one.
|
|
484
|
+
if (authInstance.samlAuthWindow) {
|
|
485
|
+
(authInstance.samlAuthWindow as any).closed = true;
|
|
486
|
+
}
|
|
487
|
+
const popup = { closed: false, focus: jest.fn(), close: jest.fn() };
|
|
488
|
+
global.window.open = jest.fn().mockReturnValue(popup);
|
|
482
489
|
(authInstance as any).samlCompletionPromise = null;
|
|
483
490
|
|
|
484
|
-
let
|
|
491
|
+
let messageHandler: ((e: any) => void) | null = null;
|
|
485
492
|
jest.spyOn(window, 'addEventListener').mockImplementation((type: any, handler: any) => {
|
|
486
493
|
if (type === 'message') {
|
|
487
|
-
|
|
494
|
+
messageHandler = handler;
|
|
488
495
|
}
|
|
489
496
|
});
|
|
490
|
-
|
|
491
497
|
jest.spyOn(tokenAuthService, 'isActiveService')
|
|
492
498
|
.mockReturnValueOnce(Promise.resolve(false));
|
|
493
499
|
|
|
494
500
|
const authPromise = authInstance.doSamlAuth({ ...embedConfig.doSamlAuthNoRedirect });
|
|
495
|
-
|
|
496
501
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
502
|
+
document.getElementById('ts-auth-btn').click();
|
|
503
|
+
return { popup, authPromise, messageHandler };
|
|
504
|
+
};
|
|
497
505
|
|
|
498
|
-
|
|
506
|
+
it('should store decoded accessToken in window when SAMLComplete event includes accessToken', async () => {
|
|
507
|
+
const { popup, authPromise, messageHandler } = await startSamlPopupFlow();
|
|
508
|
+
|
|
509
|
+
expect(decodeURIComponent((window.open as jest.Mock).mock.calls[0][0]))
|
|
510
|
+
.toContain('/v2/#/embed/saml-complete?hostAppOrigin=https%3A%2F%2Fhost.app.com');
|
|
499
511
|
|
|
500
512
|
const accessToken = 'my-access-token';
|
|
501
|
-
|
|
513
|
+
messageHandler!({
|
|
514
|
+
origin: new URL(thoughtSpotHost).origin,
|
|
515
|
+
source: popup,
|
|
502
516
|
data: {
|
|
503
517
|
type: EmbedEvent.SAMLComplete,
|
|
504
518
|
accessToken: encodeURIComponent(accessToken),
|
|
505
519
|
},
|
|
506
|
-
source: { close: jest.fn() },
|
|
507
520
|
});
|
|
508
521
|
|
|
509
522
|
await authPromise;
|
|
@@ -512,6 +525,29 @@ describe('Unit test for auth', () => {
|
|
|
512
525
|
checkReleaseVersionInBetaInstance.getValueFromWindow('cachedAuthToken'),
|
|
513
526
|
).toBe(accessToken);
|
|
514
527
|
expect(authInstance.loggedInStatus).toBe(true);
|
|
528
|
+
expect(popup.close).toHaveBeenCalled();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should ignore SAMLComplete from any origin or window other than the SSO popup', async () => {
|
|
532
|
+
const { popup, authPromise, messageHandler } = await startSamlPopupFlow();
|
|
533
|
+
const data = { type: EmbedEvent.SAMLComplete, accessToken: 'attacker-token' };
|
|
534
|
+
const tsOrigin = new URL(thoughtSpotHost).origin;
|
|
535
|
+
|
|
536
|
+
[
|
|
537
|
+
{ origin: 'https://evil.com', source: popup, data },
|
|
538
|
+
{ origin: tsOrigin, source: { close: jest.fn() }, data },
|
|
539
|
+
{ origin: tsOrigin, source: null, data },
|
|
540
|
+
].forEach((event) => messageHandler!(event));
|
|
541
|
+
|
|
542
|
+
expect(
|
|
543
|
+
checkReleaseVersionInBetaInstance.getValueFromWindow('cachedAuthToken'),
|
|
544
|
+
).toBeFalsy();
|
|
545
|
+
expect(popup.close).not.toHaveBeenCalled();
|
|
546
|
+
|
|
547
|
+
// The genuine completion still goes through afterwards.
|
|
548
|
+
messageHandler!({ origin: tsOrigin, source: popup, data: { type: EmbedEvent.SAMLComplete } });
|
|
549
|
+
await authPromise;
|
|
550
|
+
expect(popup.close).toHaveBeenCalled();
|
|
515
551
|
});
|
|
516
552
|
|
|
517
553
|
});
|
package/src/auth.ts
CHANGED
|
@@ -426,11 +426,18 @@ export const doBasicAuth = async (embedConfig: EmbedConfig): Promise<boolean> =>
|
|
|
426
426
|
* @param ssoURL
|
|
427
427
|
* @param triggerContainer
|
|
428
428
|
* @param triggerText
|
|
429
|
+
* @param thoughtSpotHost
|
|
429
430
|
*/
|
|
430
|
-
async function samlPopupFlow(
|
|
431
|
+
async function samlPopupFlow(
|
|
432
|
+
ssoURL: string,
|
|
433
|
+
triggerContainer: DOMSelector,
|
|
434
|
+
triggerText: string,
|
|
435
|
+
thoughtSpotHost: string,
|
|
436
|
+
) {
|
|
437
|
+
const thoughtSpotOrigin = new URL(thoughtSpotHost).origin;
|
|
431
438
|
let popupClosedCheck: NodeJS.Timeout;
|
|
432
439
|
const openPopup = () => {
|
|
433
|
-
if (samlAuthWindow
|
|
440
|
+
if (!samlAuthWindow || samlAuthWindow.closed) {
|
|
434
441
|
samlAuthWindow = window.open(
|
|
435
442
|
ssoURL,
|
|
436
443
|
'_blank',
|
|
@@ -461,7 +468,13 @@ async function samlPopupFlow(ssoURL: string, triggerContainer: DOMSelector, trig
|
|
|
461
468
|
}
|
|
462
469
|
samlCompletionPromise = samlCompletionPromise || new Promise<void>((resolve, reject) => {
|
|
463
470
|
window.addEventListener('message', (e) => {
|
|
464
|
-
|
|
471
|
+
// Only the SSO popup we opened, on the ThoughtSpot origin, may
|
|
472
|
+
// complete the flow or hand us a token; anything else could
|
|
473
|
+
// inject an attacker's token into the auth cache.
|
|
474
|
+
if (e.origin !== thoughtSpotOrigin || !samlAuthWindow || e.source !== samlAuthWindow) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (e.data?.type === EmbedEvent.SAMLComplete) {
|
|
465
478
|
if (e.data.accessToken) {
|
|
466
479
|
const decodedToken = decodeURIComponent(e.data.accessToken);
|
|
467
480
|
storeAuthTokenInCache(decodedToken);
|
|
@@ -506,7 +519,12 @@ const doSSOAuth = async (embedConfig: EmbedConfig, ssoEndPoint: string): Promise
|
|
|
506
519
|
|
|
507
520
|
const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`;
|
|
508
521
|
if (embedConfig.inPopup) {
|
|
509
|
-
await samlPopupFlow(
|
|
522
|
+
await samlPopupFlow(
|
|
523
|
+
ssoURL,
|
|
524
|
+
embedConfig.authTriggerContainer,
|
|
525
|
+
embedConfig.authTriggerText,
|
|
526
|
+
thoughtSpotHost,
|
|
527
|
+
);
|
|
510
528
|
const cachedToken = getCacheAuthToken();
|
|
511
529
|
if (cachedToken) {
|
|
512
530
|
loggedInStatus = true;
|
|
@@ -519,12 +537,23 @@ const doSSOAuth = async (embedConfig: EmbedConfig, ssoEndPoint: string): Promise
|
|
|
519
537
|
window.location.href = ssoURL;
|
|
520
538
|
};
|
|
521
539
|
|
|
540
|
+
/**
|
|
541
|
+
* The popup's SSO completion page. hostAppOrigin tells ThoughtSpot which
|
|
542
|
+
* origin opened the popup; the cluster only posts a token back to it if it
|
|
543
|
+
* is an allowed redirect domain.
|
|
544
|
+
* @param thoughtSpotHost
|
|
545
|
+
*/
|
|
546
|
+
const getSSOCompletePageUrl = (thoughtSpotHost: string): string => {
|
|
547
|
+
const hostAppOrigin = encodeURIComponent(window.location.origin);
|
|
548
|
+
return `${thoughtSpotHost}/v2/#/embed/saml-complete?hostAppOrigin=${hostAppOrigin}`;
|
|
549
|
+
};
|
|
550
|
+
|
|
522
551
|
export const doSamlAuth = async (embedConfig: EmbedConfig) => {
|
|
523
552
|
const { thoughtSpotHost } = embedConfig;
|
|
524
553
|
// redirect for SSO, when the SSO authentication is done, this page will be
|
|
525
554
|
// loaded again and the same JS will execute again.
|
|
526
555
|
const ssoRedirectUrl = embedConfig.inPopup
|
|
527
|
-
?
|
|
556
|
+
? getSSOCompletePageUrl(thoughtSpotHost)
|
|
528
557
|
: getRedirectUrl(
|
|
529
558
|
window.location.href,
|
|
530
559
|
SSO_REDIRECTION_MARKER_GUID,
|
|
@@ -543,7 +572,7 @@ export const doOIDCAuth = async (embedConfig: EmbedConfig) => {
|
|
|
543
572
|
// redirect for SSO, when the SSO authentication is done, this page will be
|
|
544
573
|
// loaded again and the same JS will execute again.
|
|
545
574
|
const ssoRedirectUrl = embedConfig.noRedirect || embedConfig.inPopup
|
|
546
|
-
?
|
|
575
|
+
? getSSOCompletePageUrl(thoughtSpotHost)
|
|
547
576
|
: getRedirectUrl(
|
|
548
577
|
window.location.href,
|
|
549
578
|
SSO_REDIRECTION_MARKER_GUID,
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* stop — that breaks published SDK consumers and the host runtime validation
|
|
16
16
|
* derived from these contracts.
|
|
17
17
|
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
18
20
|
import { HostEvent, EmbedEvent, RuntimeFilterOp } from '../types';
|
|
19
21
|
import { UIPassthroughEvent } from './ui-passthrough-contracts';
|
|
20
22
|
import type {
|
|
@@ -106,6 +108,8 @@ const TYPED_HOST_EVENTS: HostEvent[] = [
|
|
|
106
108
|
HostEvent.InitSpotterVizConversation,
|
|
107
109
|
HostEvent.OpenSpotterVizPanel,
|
|
108
110
|
HostEvent.CloseSpotterVizPanel,
|
|
111
|
+
// Appearance
|
|
112
|
+
HostEvent.UpdateTheme,
|
|
109
113
|
];
|
|
110
114
|
|
|
111
115
|
// Type-level assertions: compile failures here mean contract resolution
|
|
@@ -146,6 +150,7 @@ describe('event contracts (drift guardrails)', () => {
|
|
|
146
150
|
'UpdateParameters',
|
|
147
151
|
'UpdatePersonalisedView',
|
|
148
152
|
'UpdateRuntimeFilters',
|
|
153
|
+
'UpdateTheme',
|
|
149
154
|
'addColumns',
|
|
150
155
|
'answerChartSwitcher',
|
|
151
156
|
'createMonitor',
|
|
@@ -231,17 +236,121 @@ describe('event contracts (drift guardrails)', () => {
|
|
|
231
236
|
);
|
|
232
237
|
});
|
|
233
238
|
|
|
234
|
-
test('embed event
|
|
239
|
+
test('typed embed event wire values are stable (additive-only)', () => {
|
|
235
240
|
expect(
|
|
236
241
|
[
|
|
242
|
+
EmbedEvent.AIHighlights,
|
|
243
|
+
EmbedEvent.AddRemoveColumns,
|
|
244
|
+
EmbedEvent.AddToCoaching,
|
|
245
|
+
EmbedEvent.AddToFavorites,
|
|
246
|
+
EmbedEvent.AnswerChartSwitcher,
|
|
247
|
+
EmbedEvent.AnswerDelete,
|
|
237
248
|
EmbedEvent.AuthInit,
|
|
238
|
-
EmbedEvent.
|
|
249
|
+
EmbedEvent.Cancel,
|
|
250
|
+
EmbedEvent.CopyLink,
|
|
251
|
+
EmbedEvent.CopyToClipboard,
|
|
252
|
+
EmbedEvent.CrossFilterChanged,
|
|
239
253
|
EmbedEvent.CustomAction,
|
|
254
|
+
EmbedEvent.Data,
|
|
255
|
+
EmbedEvent.DataSourceSelected,
|
|
256
|
+
EmbedEvent.Delete,
|
|
257
|
+
EmbedEvent.DialogClose,
|
|
258
|
+
EmbedEvent.DialogOpen,
|
|
259
|
+
EmbedEvent.Download,
|
|
260
|
+
EmbedEvent.DownloadAsCsv,
|
|
261
|
+
EmbedEvent.DownloadAsPdf,
|
|
262
|
+
EmbedEvent.DownloadAsPng,
|
|
263
|
+
EmbedEvent.DownloadAsXlsx,
|
|
264
|
+
EmbedEvent.DownloadLiveboardAsContinuousPDF,
|
|
265
|
+
EmbedEvent.Drilldown,
|
|
266
|
+
EmbedEvent.Edit,
|
|
267
|
+
EmbedEvent.EditTML,
|
|
268
|
+
EmbedEvent.EmbedIframeCenter,
|
|
269
|
+
EmbedEvent.EmbedListenerReady,
|
|
270
|
+
EmbedEvent.Error,
|
|
271
|
+
EmbedEvent.ExitPresentMode,
|
|
272
|
+
EmbedEvent.Explore,
|
|
273
|
+
EmbedEvent.ExportTML,
|
|
274
|
+
EmbedEvent.FilterChanged,
|
|
275
|
+
EmbedEvent.LiveboardInfo,
|
|
276
|
+
EmbedEvent.LiveboardRendered,
|
|
277
|
+
EmbedEvent.MakeACopy,
|
|
278
|
+
EmbedEvent.ParameterChanged,
|
|
279
|
+
EmbedEvent.Pin,
|
|
280
|
+
EmbedEvent.Present,
|
|
281
|
+
EmbedEvent.QueryChanged,
|
|
282
|
+
EmbedEvent.Rename,
|
|
283
|
+
EmbedEvent.RequestVisibleEmbedCoordinates,
|
|
284
|
+
EmbedEvent.Save,
|
|
285
|
+
EmbedEvent.SavePersonalisedView,
|
|
286
|
+
EmbedEvent.Schedule,
|
|
287
|
+
EmbedEvent.SchedulesList,
|
|
288
|
+
EmbedEvent.SendTestScheduleEmail,
|
|
289
|
+
EmbedEvent.ShowUnderlyingData,
|
|
290
|
+
EmbedEvent.SpotIQAnalyze,
|
|
291
|
+
EmbedEvent.TableVizRendered,
|
|
292
|
+
EmbedEvent.UpdatePersonalisedView,
|
|
293
|
+
EmbedEvent.UpdateTML,
|
|
294
|
+
EmbedEvent.VizPointClick,
|
|
295
|
+
EmbedEvent.VizPointDoubleClick,
|
|
296
|
+
EmbedEvent.VizPointRightClick,
|
|
240
297
|
].map((e) => `${e}`).sort(),
|
|
241
298
|
).toEqual([
|
|
299
|
+
'AIHighlights',
|
|
300
|
+
'EmbedIframeCenter',
|
|
242
301
|
'EmbedListenerReady',
|
|
302
|
+
'Error',
|
|
303
|
+
'PinboardRendered',
|
|
304
|
+
'TableVizRendered',
|
|
305
|
+
'addRemoveColumns',
|
|
306
|
+
'addToCoaching',
|
|
307
|
+
'addToFavorites',
|
|
308
|
+
'answerChartSwitcher',
|
|
309
|
+
'answerDelete',
|
|
243
310
|
'authInit',
|
|
311
|
+
'cancel',
|
|
312
|
+
'context-menu-item-copy-to-clipboard',
|
|
313
|
+
'cross-filter-changed',
|
|
244
314
|
'customAction',
|
|
315
|
+
'data',
|
|
316
|
+
'dataSourceSelected',
|
|
317
|
+
'delete',
|
|
318
|
+
'dialog-close',
|
|
319
|
+
'dialog-open',
|
|
320
|
+
'download',
|
|
321
|
+
'downloadAsCsv',
|
|
322
|
+
'downloadAsPdf',
|
|
323
|
+
'downloadAsPng',
|
|
324
|
+
'downloadAsXlsx',
|
|
325
|
+
'downloadLiveboardAsContinuousPDF',
|
|
326
|
+
'drillDown',
|
|
327
|
+
'edit',
|
|
328
|
+
'editTSL',
|
|
329
|
+
'embedDocument',
|
|
330
|
+
'exitPresentMode',
|
|
331
|
+
'explore',
|
|
332
|
+
'exportTSL',
|
|
333
|
+
'filterChanged',
|
|
334
|
+
'makeACopy',
|
|
335
|
+
'parameterChanged',
|
|
336
|
+
'pin',
|
|
337
|
+
'pinboardInfo',
|
|
338
|
+
'present',
|
|
339
|
+
'queryChanged',
|
|
340
|
+
'rename',
|
|
341
|
+
'requestVisibleEmbedCoordinates',
|
|
342
|
+
'save',
|
|
343
|
+
'savePersonalisedView',
|
|
344
|
+
'schedule-list',
|
|
345
|
+
'sendTestScheduleEmail',
|
|
346
|
+
'showUnderlyingData',
|
|
347
|
+
'spotIQAnalyze',
|
|
348
|
+
'subscription',
|
|
349
|
+
'updatePersonalisedView',
|
|
350
|
+
'updateTSL',
|
|
351
|
+
'vizPointClick',
|
|
352
|
+
'vizPointDoubleClick',
|
|
353
|
+
'vizPointRightClick',
|
|
245
354
|
]);
|
|
246
355
|
});
|
|
247
356
|
|
|
@@ -259,7 +368,7 @@ describe('event contracts (drift guardrails)', () => {
|
|
|
259
368
|
const svc: AnswerServiceField = undefined;
|
|
260
369
|
expect(svc).toBeUndefined();
|
|
261
370
|
// Untyped embed event stays `any` (backward compatible).
|
|
262
|
-
const untyped: EmbedEventData<EmbedEvent.
|
|
371
|
+
const untyped: EmbedEventData<EmbedEvent.RouteChange> = { anything: 'goes' };
|
|
263
372
|
expect(untyped).toBeDefined();
|
|
264
373
|
});
|
|
265
374
|
|
|
@@ -320,6 +429,7 @@ describe('event contracts (drift guardrails)', () => {
|
|
|
320
429
|
],
|
|
321
430
|
}),
|
|
322
431
|
trigger(HostEvent.SpotterSearch, { query: 'revenue per year', executeSearch: true }),
|
|
432
|
+
trigger(HostEvent.UpdateTheme, { isDarkMode: true }),
|
|
323
433
|
];
|
|
324
434
|
expect(ok.every(Boolean)).toBe(true);
|
|
325
435
|
});
|
|
@@ -389,4 +499,77 @@ describe('event contracts (drift guardrails)', () => {
|
|
|
389
499
|
const all = [valid, empty, loose, topExtra, nestedExtra, zeroOverlap, bad];
|
|
390
500
|
expect([...all, voidEmpty, unmapped].every(Boolean)).toBe(true);
|
|
391
501
|
});
|
|
502
|
+
|
|
503
|
+
// Doc <-> type drift guard. Event enum members document their payload with a
|
|
504
|
+
// `Payload: {@link XxxRequest}` / `{@link XxxPayload}` TypeDoc link. TypeScript
|
|
505
|
+
// does NOT validate {@link} targets, so documenting an event's payload without
|
|
506
|
+
// declaring the type ships a dangling doc link and an untyped event. This test
|
|
507
|
+
// fails in exactly that case: every payload link in types.ts must resolve to a
|
|
508
|
+
// real declared contract type.
|
|
509
|
+
test('event doc payload links resolve to a declared type (docs and types must agree)', () => {
|
|
510
|
+
const contractsDir = __dirname;
|
|
511
|
+
const sources = [
|
|
512
|
+
fs.readFileSync(path.join(contractsDir, '../types.ts'), 'utf8'),
|
|
513
|
+
...fs
|
|
514
|
+
.readdirSync(contractsDir)
|
|
515
|
+
.filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts'))
|
|
516
|
+
.map((f) => fs.readFileSync(path.join(contractsDir, f), 'utf8')),
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
// Every type/interface/enum/class declared across types.ts + contracts.
|
|
520
|
+
const declared = new Set<string>();
|
|
521
|
+
const declRe = /(?:export\s+)?(?:declare\s+)?(?:abstract\s+)?(?:interface|type|enum|class)\s+(\w+)/g;
|
|
522
|
+
for (const src of sources) {
|
|
523
|
+
for (const m of src.matchAll(declRe)) declared.add(m[1]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Payload-shaped {@link} references in the event docs (types.ts).
|
|
527
|
+
const typesSrc = sources[0];
|
|
528
|
+
const linkRe = /\{@link\s+(\w+(?:Request|Payload|Scope))\b[^}]*\}/g;
|
|
529
|
+
const dangling = new Set<string>();
|
|
530
|
+
for (const m of typesSrc.matchAll(linkRe)) {
|
|
531
|
+
if (!declared.has(m[1])) dangling.add(m[1]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// If this fails, a doc links a payload type that does not exist — add the
|
|
535
|
+
// type (and map it), or fix the link.
|
|
536
|
+
expect([...dangling]).toEqual([]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Doc drives the type: if an event's TypeDoc documents a payload with a
|
|
540
|
+
// `Payload: {@link X}` link, that event MUST be typed in EmbedEventDataExtension.
|
|
541
|
+
// Otherwise the published doc promises a payload type while `on()` still hands
|
|
542
|
+
// back `any`. Adding the doc link is the trigger; this fails until the event is
|
|
543
|
+
// added to the map.
|
|
544
|
+
test('every embed event with a Payload {@link} is typed in the map', () => {
|
|
545
|
+
const typesSrc = fs.readFileSync(
|
|
546
|
+
path.join(__dirname, '..', 'types.ts'), 'utf8',
|
|
547
|
+
);
|
|
548
|
+
const payloadsSrc = fs.readFileSync(
|
|
549
|
+
path.join(__dirname, 'embed-event-payloads.ts'), 'utf8',
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Events typed in the map.
|
|
553
|
+
const mapBody = payloadsSrc.match(
|
|
554
|
+
/EmbedEventDataExtension\s*\{([\s\S]*?)\n\}/,
|
|
555
|
+
)?.[1] ?? '';
|
|
556
|
+
const typedMembers = new Set(
|
|
557
|
+
[...mapBody.matchAll(/\[EmbedEvent\.(\w+)\]/g)].map((m) => m[1]),
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
// EmbedEvent members whose doc carries a `Payload: {@link ...}` link.
|
|
561
|
+
const enumBody = typesSrc.match(
|
|
562
|
+
/export enum EmbedEvent\s*\{([\s\S]*?)\n\}/,
|
|
563
|
+
)?.[1] ?? '';
|
|
564
|
+
const documentedWithLink: string[] = [];
|
|
565
|
+
for (const m of enumBody.matchAll(/\/\*\*([\s\S]*?)\*\/\s*(\w+)\s*=/g)) {
|
|
566
|
+
if (/Payload:\s*\{@link/.test(m[1])) documentedWithLink.push(m[2]);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Documented a payload type but not mapped -> on() still returns `any`.
|
|
570
|
+
const documentedButUntyped = documentedWithLink.filter(
|
|
571
|
+
(name) => !typedMembers.has(name),
|
|
572
|
+
);
|
|
573
|
+
expect(documentedButUntyped).toEqual([]);
|
|
574
|
+
});
|
|
392
575
|
});
|