From 26ce1f98a979e7ed4706c7a342c139e0dc7e0571 Mon Sep 17 00:00:00 2001 From: Ashish Shubham Date: Thu, 6 Jun 2024 15:45:06 -0700 Subject: [PATCH 1/2] Use promise-postmessage --- .npmrc | 1 + jest.config.sdk.js | 19 +- package-lock.json | 255 ++++++++++++- package.json | 4 +- src/main/custom-chart-context.spec.ts | 381 ++++++-------------- src/main/custom-chart-context.ts | 67 ++-- src/main/post-message-event-bridge.spec.ts | 3 - src/main/post-message-event-bridge.ts | 98 ++--- src/main/util.ts | 11 + src/react/use-custom-chart-context.spec.tsx | 204 +++++------ src/types/ts-to-chart-event.types.ts | 7 + vite.config.ts | 9 +- 12 files changed, 557 insertions(+), 502 deletions(-) create mode 100644 .npmrc create mode 100644 src/main/util.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..0453efc --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org \ No newline at end of file diff --git a/jest.config.sdk.js b/jest.config.sdk.js index d5c5ab8..61a186a 100644 --- a/jest.config.sdk.js +++ b/jest.config.sdk.js @@ -7,7 +7,7 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '/test/', '/*.types.ts'], coverageThreshold: { './src/main': { - branches: 77, // make this above 80 + branches: 75, // make this above 80 functions: 90, lines: 92, }, @@ -22,5 +22,22 @@ module.exports = { protocol: 'https:', }, }, + 'ts-jest': { + diagnostics: { + ignoreCodes: [1343], + }, + astTransformers: { + before: [ + { + path: 'node_modules/ts-jest-mock-import-meta', // or, alternatively, 'ts-jest-mock-import-meta' directly, without node_modules. + options: { + metaObjectReplacement: { + url: 'https://www.url.com', + }, + }, + }, + ], + }, + }, }, }; diff --git a/package-lock.json b/package-lock.json index 6e99da1..c18c0cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@thoughtspot/ts-chart-sdk", - "version": "0.0.1-alpha.12", + "version": "0.0.2-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@thoughtspot/ts-chart-sdk", - "version": "0.0.1-alpha.12", + "version": "0.0.2-alpha.1", "license": "ThoughtSpot Development Tools End User License Agreement", "dependencies": { "lodash": "^4.17.21", + "promise-postmessage": "^3.5.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, @@ -49,6 +50,7 @@ "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "ts-jest": "^27.1.1", + "ts-jest-mock-import-meta": "^1.2.0", "typedoc": "^0.24.4", "typescript": "^4.9.5", "vite": "4.2.1" @@ -1504,6 +1506,198 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -1656,6 +1850,11 @@ "integrity": "sha512-AUmj9JHuHTD94slY1WR1VulFxRGC6D1pcNCN0MCulKFyiihvV/28lLS8oRHgfmc2Cxq954J8Vmosa8qzm7PLGQ==", "dev": true }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, "node_modules/@types/glob": { "version": "5.0.30", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.30.tgz", @@ -4937,7 +5136,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -8186,6 +8384,48 @@ "node": ">=0.4.0" } }, + "node_modules/promise-postmessage": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/promise-postmessage/-/promise-postmessage-3.5.0.tgz", + "integrity": "sha512-/y8MRz/mJeo3befIeh7xpZBDmTyBLN2Y1YEoW8R6oN90GRey8+H2pMzDWAyjSbIzrcC1gr/qJHW798PvjW+7RQ==", + "dependencies": { + "rollup": "^4.0.2" + } + }, + "node_modules/promise-postmessage/node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9432,6 +9672,15 @@ } } }, + "node_modules/ts-jest-mock-import-meta": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-jest-mock-import-meta/-/ts-jest-mock-import-meta-1.2.0.tgz", + "integrity": "sha512-r2+TH6d8LHBXqLTXjJh1KfTZoMvGV0hdn9gwickNVcwS2Co2/mewGjj0XDVEPLg5MVfZVHUFQ9O09anURA3KCw==", + "dev": true, + "peerDependencies": { + "ts-jest": ">=20.0.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", diff --git a/package.json b/package.json index 6235385..1ff17a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@thoughtspot/ts-chart-sdk", "private": false, - "version": "0.0.1-alpha.12", + "version": "0.0.2-alpha.1", "module": "lib/index", "main": "lib/index", "types": "lib/index", @@ -68,12 +68,14 @@ "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "ts-jest": "^27.1.1", + "ts-jest-mock-import-meta": "^1.2.0", "typedoc": "^0.24.4", "typescript": "^4.9.5", "vite": "4.2.1" }, "dependencies": { "lodash": "^4.17.21", + "promise-postmessage": "^3.5.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/src/main/custom-chart-context.spec.ts b/src/main/custom-chart-context.spec.ts index 2d341ae..b9c296e 100644 --- a/src/main/custom-chart-context.spec.ts +++ b/src/main/custom-chart-context.spec.ts @@ -11,7 +11,7 @@ import { TSToChartEvent } from '../types/ts-to-chart-event.types'; import { CustomChartContext, getChartContext } from './custom-chart-context'; import * as PostMessageEventBridge from './post-message-event-bridge'; -jest.mock('./post-message-event-bridge'); +// jest.mock('./post-message-event-bridge'); jest.spyOn(console, 'log').mockImplementation(() => { // do nothing. @@ -39,6 +39,7 @@ describe('CustomChartContext', () => { mockInitMessage.mockImplementation((fn: any) => { eventProcessor = fn; + return () => null; }); mockPostMessageToHost.mockImplementation(() => { @@ -85,29 +86,20 @@ describe('CustomChartContext', () => { const promise = customChartContext.initialize(); // Check that the hasInitializedPromise has resolved - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + + const initResp = await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); await expect(promise).resolves.toBeUndefined(); - expect(mockPostMessage).toHaveBeenCalledTimes(2); - expect(mockPostMessage.mock.calls[0][0]).toEqual({ + expect(initResp).toEqual({ isConfigValid: false, defaultChartConfig: undefined, chartConfigEditorDefinition: undefined, @@ -117,7 +109,6 @@ describe('CustomChartContext', () => { allowColumnConditionalFormatting: false, }, }); - expect(mockPostMessage).toHaveBeenCalled(); expect(mockPostMessageToHost).not.toHaveBeenCalled(); }); @@ -126,23 +117,14 @@ describe('CustomChartContext', () => { const promise = customChartContext.initialize(); // Check that the hasInitializedPromise has resolved - const mockPostMessage = jest.fn(); eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); await expect(promise).resolves.toBeUndefined(); @@ -188,126 +170,75 @@ describe('CustomChartContext', () => { eventProcessor = null; }); - test('should not trigger post message if host is not accurate', () => { - expect(mockInitMessage).toHaveBeenCalled(); - - // mock the event trigger for ChartConfigValidate - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.ChartConfigValidate, - source: 'incorrect-source', - }, - ports: [{ postMessage: mockPostMessage }], - }); - // Check that the event listener was added to the eventListeners - // object - expect(mockPostMessage).not.toHaveBeenCalled(); - }); - - test('default internal function testing', () => { + test('default internal function testing', async () => { expect(mockInitMessage).toHaveBeenCalled(); // mock the event trigger for ChartConfigValidate - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.ChartConfigValidate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + const validateChartConfigResp = await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.ChartConfigValidate, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(validateChartConfigResp).toEqual({ isValid: true, }); - mockPostMessage.mockReset(); // mock the event trigger for VisualPropsValidate - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.VisualPropsValidate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + const validateVisualPropsResp = await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.VisualPropsValidate, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(validateVisualPropsResp).toEqual({ isValid: true, }); - mockPostMessage.mockReset(); - // mock the event trigger for TriggerRenderChart - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.TriggerRenderChart, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + const renderResp = await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.TriggerRenderChart, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({}); expect(renderChart).toHaveBeenCalled(); - mockPostMessage.mockReset(); - // mock the event trigger for getQueriesFromChartConfig eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.GetDataQuery, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.GetDataQuery, }); - // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({}); + expect(getQueriesFromChartConfig).toHaveBeenCalled(); }); - test('default external function testing', () => { + test('default external function testing', async () => { expect(mockInitMessage).toHaveBeenCalled(); // mock the event trigger for DataUpdate - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: { - data: 'random data', - }, - eventType: TSToChartEvent.DataUpdate, - source: 'ts-host-app', + + const dataUpdateResp = await eventProcessor({ + payload: { + data: 'random data', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.DataUpdate, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(dataUpdateResp).toEqual({ triggerRenderChart: true, }); expect(customChartContext.getChartModel().data).toBe('random data'); - mockPostMessage.mockReset(); // mock the event trigger for VisualPropsValidate - eventProcessor({ - data: { - payload: { - chartModel: { - data: 'random data2', - visualProps: null, - }, + const modelUpdateResp = await eventProcessor({ + payload: { + chartModel: { + data: 'random data2', + visualProps: null, }, - eventType: TSToChartEvent.ChartModelUpdate, - source: 'ts-host-app', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.ChartModelUpdate, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(modelUpdateResp).toEqual({ triggerRenderChart: true, }); expect(customChartContext.getChartModel().data).toBe( @@ -315,27 +246,20 @@ describe('CustomChartContext', () => { ); expect(customChartContext.getChartModel().visualProps).toBe(null); - mockPostMessage.mockReset(); - // mock the event trigger for VisualPropsUpdate - eventProcessor({ - data: { - payload: { - visualProps: 'random data', - }, - eventType: TSToChartEvent.VisualPropsUpdate, - source: 'ts-host-app', + const visualPropsUpdateResp = await eventProcessor({ + payload: { + visualProps: 'random data', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.VisualPropsUpdate, }); // Check that the response was received - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(visualPropsUpdateResp).toEqual({ triggerRenderChart: true, }); expect(customChartContext.getChartModel().visualProps).toBe( 'random data', ); - mockPostMessage.mockReset(); // mock the event trigger for AxisMenuActionClick customChartContext.axisMenuCustomActionPreProcessor([ @@ -350,22 +274,18 @@ describe('CustomChartContext', () => { ], }, ] as any); - eventProcessor({ - data: { - payload: { - customAction: { - id: 'custom-action-1', - columnIds: ['9f96b5b0-f7e4-4a5e-aa11-4c77fdf42125'], - }, + const axisMenuClickResp = await eventProcessor({ + payload: { + customAction: { + id: 'custom-action-1', + columnIds: ['9f96b5b0-f7e4-4a5e-aa11-4c77fdf42125'], }, - eventType: TSToChartEvent.AxisMenuActionClick, - source: 'ts-host-app', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.AxisMenuActionClick, }); // Check that the response was valid which means action gets // executed successfully - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(axisMenuClickResp).toEqual({ isValid: true, }); @@ -382,22 +302,18 @@ describe('CustomChartContext', () => { ], }, ] as any); - eventProcessor({ - data: { - payload: { - customAction: { - id: 'custom-action-1', - columnIds: ['9f96b5b0-f7e4-4a5e-aa11-4c77fdf42125'], - }, + const ctxMenuResp = await eventProcessor({ + payload: { + customAction: { + id: 'custom-action-1', + columnIds: ['9f96b5b0-f7e4-4a5e-aa11-4c77fdf42125'], }, - eventType: TSToChartEvent.ContextMenuActionClick, - source: 'ts-host-app', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.ContextMenuActionClick, }); // Check that the response was valid which means action gets // executed successfully - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(ctxMenuResp).toEqual({ isValid: true, }); }); @@ -412,38 +328,30 @@ describe('CustomChartContext', () => { customChartContext.on(TEST_EVENT_TYPE, testCallbackFn); // mock the event trigger - const mockPostMessage = jest.fn(); + eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TEST_EVENT_TYPE, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TEST_EVENT_TYPE, }); // Check that the event listener was added to the eventListeners // object expect(testCallbackFn).toHaveBeenCalled(); }); - test('should respond with an error to an unspecified event type', () => { + test('should respond with an error to an unspecified event type', async () => { expect(mockInitMessage).toHaveBeenCalled(); // Define a test event type and callback function const TEST_EVENT_TYPE = 'testEventType' as any; // mock the event trigger - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TEST_EVENT_TYPE, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + + const resp = await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TEST_EVENT_TYPE, }); // Check that the event listener was added to the eventListeners // object - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(resp).toEqual({ hasError: true, error: `Event type not recognised or processed: ${TEST_EVENT_TYPE}`, }); @@ -477,14 +385,10 @@ describe('CustomChartContext', () => { customChartContext.on(TEST_EVENT_TYPE, testCallbackFn); // mock the event trigger for the test event type - const mockPostMessage = jest.fn(); + eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TEST_EVENT_TYPE, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TEST_EVENT_TYPE, }); // Call the off function to remove the event listener @@ -518,22 +422,14 @@ describe('CustomChartContext', () => { ).toHaveLength(1); // Emit events for both event types - const mockPostMessage = jest.fn(); + eventProcessor({ - data: { - payload: {}, - eventType: TEST_EVENT_TYPE_1, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TEST_EVENT_TYPE_1, }); eventProcessor({ - data: { - payload: {}, - eventType: TEST_EVENT_TYPE_2, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TEST_EVENT_TYPE_2, }); // Check that both callback functions were called @@ -600,28 +496,19 @@ describe('CustomChartContext', () => { test('should resolve the promise if the chart context is initialized', async () => { // Check that the hasInitializedPromise has resolved - const mockPostMessage = jest.fn(); + eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); const promise = customChartContext.initialize(); await expect(promise).resolves.toBeUndefined(); - expect(mockPostMessage).toHaveBeenCalledTimes(2); // define mock response for the postMessage response promise let resolve: any; @@ -676,24 +563,15 @@ describe('CustomChartContext', () => { ); }); test('should process the event payload for context menu custom actions', async () => { - const mockPostMessage = jest.fn(); const mockCustomAction = jest.fn(); let resolve: any; eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); customChartContext.initialize(); mockPostMessageToHost.mockImplementation( @@ -735,19 +613,15 @@ describe('CustomChartContext', () => { TEST_EVENT_TYPE, ); eventProcessor({ - data: { - payload: { - customAction: { - id: 'custom-action-1', - clickedPoint: {}, - event: {}, - selectedPoints: [{}], - }, + payload: { + customAction: { + id: 'custom-action-1', + clickedPoint: {}, + event: {}, + selectedPoints: [{}], }, - eventType: TSToChartEvent.ContextMenuActionClick, - source: 'ts-host-app', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.ContextMenuActionClick, }); expect(mockCustomAction).toHaveBeenCalled(); expect(mockCustomAction).toHaveBeenCalledWith({ @@ -758,24 +632,15 @@ describe('CustomChartContext', () => { }); }); test('should process the event payload for axis menu custom actions', async () => { - const mockPostMessage = jest.fn(); const mockCustomAction = jest.fn(); let resolve: any; eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); customChartContext.initialize(); mockPostMessageToHost.mockImplementation( @@ -817,18 +682,14 @@ describe('CustomChartContext', () => { ); eventProcessor({ - data: { - payload: { - customAction: { - id: 'custom-action-1', - columnIds: ['123'], - event: {}, - }, + payload: { + customAction: { + id: 'custom-action-1', + columnIds: ['123'], + event: {}, }, - eventType: TSToChartEvent.AxisMenuActionClick, - source: 'ts-host-app', }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.AxisMenuActionClick, }); expect(mockCustomAction).toHaveBeenCalled(); expect(mockCustomAction).toHaveBeenCalledWith({ @@ -839,23 +700,14 @@ describe('CustomChartContext', () => { }); test('should process the event payload for axis menu when custom actions are not defined', async () => { - const mockPostMessage = jest.fn(); let resolve: any; eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); customChartContext.initialize(); mockPostMessageToHost.mockImplementation( @@ -880,23 +732,14 @@ describe('CustomChartContext', () => { }); test('should process the event payload for context menu when custom actions are not defined', async () => { - const mockPostMessage = jest.fn(); let resolve: any; eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + payload: {}, + eventType: TSToChartEvent.InitializeComplete, }); customChartContext.initialize(); mockPostMessageToHost.mockImplementation( diff --git a/src/main/custom-chart-context.ts b/src/main/custom-chart-context.ts index 4b87b23..69a42ee 100644 --- a/src/main/custom-chart-context.ts +++ b/src/main/custom-chart-context.ts @@ -43,9 +43,11 @@ import { VisualPropsValidateEventPayload, } from '../types/ts-to-chart-event.types'; import { VisualPropEditorDefinition } from '../types/visual-prop.types'; -import * as PostMessageEventBridge from './post-message-event-bridge'; - -let isInitialized = false; +import { + globalThis, + initMessageListener, + postMessageToHostApp, +} from './post-message-event-bridge'; export type AllowedConfigurations = { allowColumnNumberFormatting: boolean; @@ -156,6 +158,8 @@ export class CustomChartContext { */ private componentId = ''; + private removeListener: () => void = _.noop; + /** * host app url * @@ -232,6 +236,8 @@ export class CustomChartContext { */ private axisMenuActionHandler: AxisMenuActionHandler = {}; + public containerEl: HTMLElement | null = null; + /** * Constructor to only accept context props as payload * @@ -320,8 +326,8 @@ export class CustomChartContext { * @version SDK: 0.1 | ThoughtSpot: */ public destroy() { - PostMessageEventBridge.destroyMessageListener(this.eventProcessor); - isInitialized = false; + this.removeListener(); + globalThis.isInitialized = false; } /** @@ -444,7 +450,7 @@ export class CustomChartContext { eventType: T, ...eventPayload: ChartToTSEventsPayloadMap[T] ): Promise { - if (!isInitialized) { + if (!globalThis.isInitialized) { console.log( 'Chart Context: not initialized the context, something went wrong', ); @@ -454,7 +460,7 @@ export class CustomChartContext { eventType, eventPayload, ); - return PostMessageEventBridge.postMessageToHostApp( + return postMessageToHostApp( this.componentId, this.hostUrl, processedPayload?.[0] ?? null, @@ -467,13 +473,13 @@ export class CustomChartContext { * Process all the functions via the eventProcess callback */ private registerEventProcessor = () => { - if (isInitialized) { + if (globalThis.isInitialized) { console.error( 'The context is already initialized. you cannot have multiple contexts', ); throw new Error(ErrorType.MultipleContextsNotSupported); } - PostMessageEventBridge.initMessageListener(this.eventProcessor); + this.removeListener = initMessageListener(this.eventProcessor); this.registerEvents(); }; @@ -483,29 +489,13 @@ export class CustomChartContext { * * @param event : Message Event Object */ - private eventProcessor = (event: MessageEvent) => { - if (event.data.source !== 'ts-host-app') { - return; - } + private eventProcessor = (data: any) => { + console.log('Chart Context: message received:', data.eventType, data); - console.log( - 'Chart Context: message received:', - event.data.eventType, - event.data, - ); - - const messageResponse = this.executeEventListenerCBs(event); + const messageResponse = this.executeEventListenerCBs(data); // respond back to parent to confirm/ack the receipt - if (!_.isNil(event.ports[0])) { - if (!_.isNil(messageResponse)) { - event.ports[0].postMessage({ - ...(messageResponse as any), - }); - } else { - event.ports[0].postMessage({}); - } - } + return messageResponse; }; /** @@ -759,13 +749,16 @@ export class CustomChartContext { this.hostUrl = payload.hostUrl; this.chartModel = payload.chartModel; this.appConfig = payload.appConfig ?? {}; + this.containerEl = payload.containerElSelector + ? document.querySelector(payload.containerElSelector) + : null; return this.publishChartContextPropsToHost(); }; private initializationComplete = (): void => { // context is now initialized - isInitialized = true; + globalThis.isInitialized = true; // TODO: following can be done behind a promise this.triggerInitResolve(); @@ -810,12 +803,12 @@ export class CustomChartContext { * @param event : Message Event Object * @returns response to be sent back to the message sender (host) */ - private executeEventListenerCBs = (event: MessageEvent): any => { + private executeEventListenerCBs = (data: any): any => { // do basic sanity - const payload = event.data.payload; + const payload = data.payload; let response; - if (_.isArray(this.eventListeners[event.data.eventType])) { - this.eventListeners[event.data.eventType].forEach((callback) => { + if (_.isArray(this.eventListeners[data.eventType])) { + this.eventListeners[data.eventType].forEach((callback) => { // this is a problem today if we have multiple callbacks // registered. only the last response will be sent back to the // server @@ -824,15 +817,15 @@ export class CustomChartContext { } else { response = { hasError: true, - error: `Event type not recognised or processed: ${event.data.eventType}`, + error: `Event type not recognised or processed: ${data.eventType}`, }; } console.log( 'ChartContext: Response:', - event.data.eventType, + data.eventType, response, - this.eventListeners[event.data.eventType]?.length, + this.eventListeners[data.eventType]?.length, ); return response; }; diff --git a/src/main/post-message-event-bridge.spec.ts b/src/main/post-message-event-bridge.spec.ts index 6a34c67..8d5ca4c 100644 --- a/src/main/post-message-event-bridge.spec.ts +++ b/src/main/post-message-event-bridge.spec.ts @@ -66,7 +66,6 @@ describe('postMessageToHostApp', () => { ); // Verify that the MessageChannel was used correctly - expect(channel.port1.close).toHaveBeenCalled(); expect(messageChannelMock.MessageChannel).toHaveBeenCalled(); }); @@ -121,8 +120,6 @@ describe('postMessageToHostApp', () => { [channel.port2], ); - // Verify that the MessageChannel was used correctly - expect(channel.port1.close).toHaveBeenCalled(); expect(messageChannelMock.MessageChannel).toHaveBeenCalled(); }); diff --git a/src/main/post-message-event-bridge.ts b/src/main/post-message-event-bridge.ts index 298c7fd..6a5fc24 100644 --- a/src/main/post-message-event-bridge.ts +++ b/src/main/post-message-event-bridge.ts @@ -1,23 +1,23 @@ +import { onMessage, sendMessage } from 'promise-postmessage'; import { ChartToTSEvent } from '../types/chart-to-ts-event.types'; +import { timeout } from './util'; const TIMEOUT_THRESHOLD = 30000; // 30sec -/** - * method to listen to messages using postMessage from the parent. - * - * @param {any} handleMessageEvent - */ -const init = (handleMessageEvent: any) => { - window.addEventListener('message', handleMessageEvent); -}; +const elSelector = new URL(import.meta.url).searchParams.get('elSelector'); +const target = + (elSelector && (document.querySelector(elSelector) as HTMLElement)) || + window.parent; + +const globalThis = (target instanceof Window ? window : target) as any; /** - * stop listening to the messages + * method to listen to messages using postMessage from the parent. * * @param {any} handleMessageEvent */ -const destroy = (handleMessageEvent: any) => { - window.removeEventListener('message', handleMessageEvent); +const init = (handleMessageEvent: (data: any) => Promise | any) => { + return onMessage(handleMessageEvent, target, 'child'); }; /** @@ -28,61 +28,35 @@ const destroy = (handleMessageEvent: any) => { * @param {ChartToTSEvent} eventType type of the event * @returns Promise */ -const postMessageToHostApp = ( +const postMessageToHostApp = async ( componentId: string, hostUrl: string, eventPayload: any, eventType: ChartToTSEvent, -): Promise => - new Promise((resolve, reject) => { - const channel = new MessageChannel(); - channel.port1.onmessage = (res: any) => { - channel.port1.close(); - const { hasError, error } = res.data; - if (hasError) { - console.log('ChartContext: message failure:', res.data); - reject(error); - } else { - console.log('ChartContext: message success:', res.data); - resolve(res.data); - } - }; - - setTimeout(() => { - reject( - new Error( - `ChartContext: postMessage operation timed out. ${eventType}`, - eventPayload, - ), - ); - }, TIMEOUT_THRESHOLD); - - try { - window.parent.window.postMessage( - { - componentId, - payload: { - ...eventPayload, - }, - eventType, - source: 'ts-chart-sdk', +): Promise => { + const resp = await timeout( + sendMessage( + target, + { + componentId, + payload: { + ...eventPayload, }, - hostUrl, - [channel.port2], - ); - } catch (err) { - console.error( - 'ChartContext: error in emitting event:', - err, eventType, - eventPayload, - ); - reject(err); - } - }); - -export { - init as initMessageListener, - destroy as destroyMessageListener, - postMessageToHostApp, + source: 'ts-chart-sdk', + }, + { + origin: hostUrl, + endpoint: 'child', + }, + ), + TIMEOUT_THRESHOLD, + 'ChartContext: postMessage operation timed out.', + ); + if (resp.hasError) { + throw new Error(resp.error); + } + return resp; }; + +export { init as initMessageListener, postMessageToHostApp, globalThis }; diff --git a/src/main/util.ts b/src/main/util.ts new file mode 100644 index 0000000..41c3704 --- /dev/null +++ b/src/main/util.ts @@ -0,0 +1,11 @@ +export function timeout(promise: Promise, ms: number, message?: string) { + return Promise.race([ + promise, + new Promise((resolve, reject) => { + setTimeout( + () => reject(new Error(message || 'Operation timed out.')), + ms, + ); + }), + ]); +} diff --git a/src/react/use-custom-chart-context.spec.tsx b/src/react/use-custom-chart-context.spec.tsx index 7fb8cec..686d784 100644 --- a/src/react/use-custom-chart-context.spec.tsx +++ b/src/react/use-custom-chart-context.spec.tsx @@ -8,8 +8,6 @@ import { TSToChartEvent } from '../types/ts-to-chart-event.types'; import { contextChartProps } from './mocks/custom-chart-context-mock'; import { useChartContext } from './use-custom-chart-context'; -jest.mock('../main/post-message-event-bridge'); - describe('useChartContext initialization', () => { let eventProcessor: any; let mockInitMessage; @@ -26,6 +24,7 @@ describe('useChartContext initialization', () => { ); mockInitMessage.mockImplementation((fn: any) => { eventProcessor = fn; + return () => null; }); mockPostMessageToHost.mockImplementation(() => { @@ -35,6 +34,7 @@ describe('useChartContext initialization', () => { afterEach(() => { // Clear mock implementations after each test jest.clearAllMocks(); + PostMessageEventBridge.globalThis.isInitialized = false; }); test('should initialize the context only after intialize completes', async () => { @@ -47,32 +47,24 @@ describe('useChartContext initialization', () => { expect(result.current.hasInitialized).toBe(false); expect(result.current.chartModel).toBeUndefined(); const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: { - componentId: 'COMPONENT_ID', - hostUrl: 'https://some.chart.app', - chartModel: mockedChartModel, - }, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', + await eventProcessor({ + payload: { + componentId: 'COMPONENT_ID', + hostUrl: 'https://some.chart.app', + chartModel: mockedChartModel, }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.Initialize, }); - - eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: {}, + eventType: TSToChartEvent.InitializeComplete, + source: 'ts-host-app', }); + await waitFor(() => { expect(result.current.hasInitialized).toBe(true); expect(result.current.chartModel).toEqual(mockedChartModel); }); - result.current?.destroy(); }); test('should make sure hasInitialized to remain false when context initialization failed', async () => { @@ -117,6 +109,7 @@ describe('useChartContext emit', () => { ); mockInitMessage.mockImplementation((fn: any) => { eventProcessor = fn; + return () => null; }); mockPostMessageToHost.mockImplementation(() => { @@ -126,6 +119,7 @@ describe('useChartContext emit', () => { afterEach(() => { // Clear mock implementations after each test jest.clearAllMocks(); + PostMessageEventBridge.globalThis.isInitialized = false; }); test('should trigger the emitter correctly when context is initialized', async () => { @@ -134,23 +128,17 @@ describe('useChartContext emit', () => { useChartContext(contextChartProps), ); const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, }); - eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: {}, + eventType: TSToChartEvent.InitializeComplete, + source: 'ts-host-app', }); + await waitFor(() => { expect(result.current.hasInitialized).toBe(true); }); @@ -174,14 +162,12 @@ describe('useChartContext emit', () => { useChartContext(contextChartProps), ); const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, + source: 'ts-host-app', }); + await waitFor(() => { expect(result.current.hasInitialized).toBe(false); }); @@ -207,6 +193,7 @@ describe('useChartContext setOn listeners', () => { ); mockInitMessage.mockImplementation((fn: any) => { eventProcessor = fn; + return () => null; }); mockPostMessageToHost.mockImplementation(() => { @@ -216,6 +203,7 @@ describe('useChartContext setOn listeners', () => { afterEach(() => { // Clear mock implementations after each test jest.clearAllMocks(); + PostMessageEventBridge.globalThis.isInitialized = false; }); test('should trigger the setOnEvent correctly when context is initialized', async () => { @@ -227,63 +215,49 @@ describe('useChartContext setOn listeners', () => { // Assert that the context is not initialized initially expect(result.current.hasInitialized).toBe(false); expect(result.current.chartModel).toBeUndefined(); - const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: { - componentId: 'COMPONENT_ID', - hostUrl: 'https://some.chart.app', - chartModel: mockedChartModel, - }, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', + await eventProcessor({ + payload: { + componentId: 'COMPONENT_ID', + hostUrl: 'https://some.chart.app', + chartModel: mockedChartModel, }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.Initialize, }); - eventProcessor({ - data: { - payload: {}, - eventType: TSToChartEvent.InitializeComplete, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: {}, + eventType: TSToChartEvent.InitializeComplete, + source: 'ts-host-app', }); + await waitFor(() => { expect(result.current.hasInitialized).toBe(true); expect(result.current.chartModel).toEqual(mockedChartModel); }); - eventProcessor({ - data: { - payload: { chartModel: {} }, - eventType: TSToChartEvent.ChartModelUpdate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + const resp = await eventProcessor({ + payload: { chartModel: {} }, + eventType: TSToChartEvent.ChartModelUpdate, + source: 'ts-host-app', }); + await waitFor(() => { - expect(mockPostMessage).toHaveBeenCalledWith({ + expect(resp).toEqual({ triggerRenderChart: true, }); expect(result.current.chartModel).toEqual({}); }); - eventProcessor({ - data: { - payload: { visualProps: { color: 'red' } }, - eventType: TSToChartEvent.VisualPropsUpdate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + const propsUpdateResp = await eventProcessor({ + payload: { visualProps: { color: 'red' } }, + eventType: TSToChartEvent.VisualPropsUpdate, + source: 'ts-host-app', }); - expect(mockPostMessage).toHaveBeenCalledWith({ + + expect(propsUpdateResp).toEqual({ triggerRenderChart: true, }); await waitFor(() => { - expect(mockPostMessage).toHaveBeenCalledWith({ - triggerRenderChart: true, - }); expect(result.current.chartModel?.visualProps).toEqual({ color: 'red', }); @@ -297,14 +271,12 @@ describe('useChartContext setOn listeners', () => { useChartContext(contextChartProps), ); const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: mockInitializeContextPayload, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: mockInitializeContextPayload, + eventType: TSToChartEvent.Initialize, + source: 'ts-host-app', }); + await waitFor(() => { expect(result.current.hasInitialized).toBe(false); }); @@ -333,6 +305,7 @@ describe('useChartContext on React Wrapper component', () => { ); mockInitMessage.mockImplementation((fn: any) => { eventProcessor = fn; + return () => null; }); mockPostMessageToHost.mockImplementation(() => { @@ -342,6 +315,7 @@ describe('useChartContext on React Wrapper component', () => { afterEach(() => { // Clear mock implementations after each test jest.clearAllMocks(); + PostMessageEventBridge.globalThis.isInitialized = false; }); test('TSChartContext renders children and should not increase counter for useEffect on chartModel if visualProps is updated', async () => { const CustomChartComponent = () => { @@ -364,18 +338,15 @@ describe('useChartContext on React Wrapper component', () => { const { getByTestId, rerender } = render(); const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: { - componentId: 'COMPONENT_ID', - hostUrl: 'https://some.chart.app', - chartModel: mockedChartModel, - }, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', + await eventProcessor({ + payload: { + componentId: 'COMPONENT_ID', + hostUrl: 'https://some.chart.app', + chartModel: mockedChartModel, }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.Initialize, }); + rerender(); // Check if the child element is rendered @@ -385,13 +356,10 @@ describe('useChartContext on React Wrapper component', () => { ); }); - eventProcessor({ - data: { - payload: { visualProps: { color: 'red' } }, - eventType: TSToChartEvent.VisualPropsUpdate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + await eventProcessor({ + payload: { visualProps: { color: 'red' } }, + eventType: TSToChartEvent.VisualPropsUpdate, + source: 'ts-host-app', }); // Re-render the component with a new key @@ -432,26 +400,22 @@ describe('useChartContext on React Wrapper component', () => { // Re-render the component with a new key const mockPostMessage = jest.fn(); - eventProcessor({ - data: { - payload: { - componentId: 'COMPONENT_ID', - hostUrl: 'https://some.chart.app', - chartModel: mockedChartModel, - }, - eventType: TSToChartEvent.Initialize, - source: 'ts-host-app', + await eventProcessor({ + payload: { + componentId: 'COMPONENT_ID', + hostUrl: 'https://some.chart.app', + chartModel: mockedChartModel, }, - ports: [{ postMessage: mockPostMessage }], + eventType: TSToChartEvent.Initialize, + source: 'ts-host-app', }); - eventProcessor({ - data: { - payload: { visualProps: { color: 'red' } }, - eventType: TSToChartEvent.VisualPropsUpdate, - source: 'ts-host-app', - }, - ports: [{ postMessage: mockPostMessage }], + + await eventProcessor({ + payload: { visualProps: { color: 'red' } }, + eventType: TSToChartEvent.VisualPropsUpdate, + source: 'ts-host-app', }); + rerender(); // Check if the child element is still in the document after re-render const updatedChildElement = getByTestId('child-element'); diff --git a/src/types/ts-to-chart-event.types.ts b/src/types/ts-to-chart-event.types.ts index 3a128df..2461da9 100644 --- a/src/types/ts-to-chart-event.types.ts +++ b/src/types/ts-to-chart-event.types.ts @@ -160,6 +160,13 @@ export interface InitializeEventPayload { * @version SDK: 0.1 | ThoughtSpot: */ hostUrl: string; + /** + * The selector of the container element where the chart will be rendered. Used for internal + * charts, external custom charts mostly have their own DOM. + * + * @version SDK: 0.2 | ThoughtSpot: + */ + containerElSelector: string; } /** diff --git a/vite.config.ts b/vite.config.ts index 8519be6..924d1f2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,21 +3,18 @@ import type { UserConfig } from 'vite'; const config: UserConfig = { plugins: [], optimizeDeps: { - exclude: [ - 'chart.js', - ], + exclude: ['chart.js'], esbuildOptions: { treeShaking: true, }, }, define: { __IS_MICROFRONTEND__: false, - "process.env": {}, - + 'process.env': {}, }, resolve: { alias: { - '~@thoughtspot': '@thoughtspot', + '~@thoughtspot': '@thoughtspot', }, }, }; From 14546eeb6870c8c3f424b197a89143ccb61a5310 Mon Sep 17 00:00:00 2001 From: Ashish Shubham Date: Mon, 17 Jun 2024 16:51:53 -0700 Subject: [PATCH 2/2] remove cjs --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1ff17a9..5073b0b 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,13 @@ "main": "lib/index", "types": "lib/index", "files": [ - "cjs/**", "dist/**", "lib/**", "src/**" ], "scripts": { - "build": "tsc -p .; tsc -p . --module commonjs --outDir cjs", - "tsc": "tsc -p .; tsc -p . --module commonjs --outDir cjs", + "build": "tsc -p .", + "tsc": "tsc -p .", "test-sdk": "npx jest -c jest.config.sdk.js --runInBand", "test": "npm run test-sdk", "lint": "eslint 'src/**'",