From a5abe714fd3b68d81b32a39f873bd04cfe73bb25 Mon Sep 17 00:00:00 2001 From: weareoutman Date: Tue, 30 Jul 2024 11:38:09 +0800 Subject: [PATCH 1/2] fix(): add a new event action: window.postMessage --- .../src/internal/bindListeners.spec.ts | 32 +++++++++++++++++++ .../runtime/src/internal/bindListeners.ts | 23 +++++++++++-- packages/types/src/manifest.ts | 1 + 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/internal/bindListeners.spec.ts b/packages/runtime/src/internal/bindListeners.spec.ts index cc2c94de3b..c0d70e7027 100644 --- a/packages/runtime/src/internal/bindListeners.spec.ts +++ b/packages/runtime/src/internal/bindListeners.spec.ts @@ -37,6 +37,7 @@ const consoleWarn = jest.spyOn(console, "warn"); const consoleError = jest.spyOn(console, "error"); const windowOpen = jest.spyOn(window, "open"); const windowAlert = jest.spyOn(window, "alert"); +const windowPostMessage = jest.spyOn(window, "postMessage"); const mockGetHistory = getHistory as jest.Mock; const mockHandleHttpError = handleHttpError as jest.MockedFunction< typeof handleHttpError @@ -756,6 +757,37 @@ describe("listenerFactory for console.*", () => { )(event); expect(windowOpen).toBeCalledWith("/ok", "_blank", "popup=yes"); }); + + test("window.postMessage without origin", () => { + listenerFactory( + { + action: "window.postMessage", + args: ["<% { channel: 'test-1', detail: EVENT.detail } %>"], + }, + runtimeContext + )(event); + expect(windowPostMessage).toBeCalledWith( + { channel: "test-1", detail: "ok" }, + "http://localhost" + ); + }); + + test("window.postMessage with origin", () => { + listenerFactory( + { + action: "window.postMessage", + args: [ + "<% { channel: 'test-2', detail: EVENT.detail } %>", + "<% location.origin %>", + ], + }, + runtimeContext + )(event); + expect(windowPostMessage).toBeCalledWith( + { channel: "test-2", detail: "ok" }, + "http://localhost" + ); + }); }); describe("listenerFactory for setting brick properties", () => { diff --git a/packages/runtime/src/internal/bindListeners.ts b/packages/runtime/src/internal/bindListeners.ts index 71f4fdb653..bc615d75a0 100644 --- a/packages/runtime/src/internal/bindListeners.ts +++ b/packages/runtime/src/internal/bindListeners.ts @@ -162,7 +162,11 @@ export function listenerFactory( // case "alias.replace": case "window.open": - handleWindowAction(event, handler.args, runtimeContext); + handleWindowOpenAction(event, handler.args, runtimeContext); + break; + + case "window.postMessage": + handleWindowPostMessageAction(event, handler.args, runtimeContext); break; case "location.reload": @@ -589,7 +593,7 @@ function handleHistoryAction( ); } -function handleWindowAction( +function handleWindowOpenAction( event: Event, args: unknown[] | undefined, runtimeContext: RuntimeContext @@ -602,6 +606,21 @@ function handleWindowAction( window.open(url, target || "_self", features); } +function handleWindowPostMessageAction( + event: Event, + args: unknown[] | undefined, + runtimeContext: RuntimeContext +) { + const computedArgs = argsFactory(args, runtimeContext, event) as Parameters< + typeof window.postMessage + >; + if (computedArgs.length === 1) { + // Add default target origin + computedArgs.push(location.origin); + } + window.postMessage(...computedArgs); +} + function batchUpdate( args: unknown[], batch: boolean, diff --git a/packages/types/src/manifest.ts b/packages/types/src/manifest.ts index b35f234161..162bba60dd 100644 --- a/packages/types/src/manifest.ts +++ b/packages/types/src/manifest.ts @@ -944,6 +944,7 @@ export interface BuiltinBrickEventHandler { | "location.reload" | "location.assign" | "window.open" + | "window.postMessage" | "event.preventDefault" | "event.stopPropagation" | "console.log" From a2b5474f8bd5f87522a4e7afab46baa34e7d32f2 Mon Sep 17 00:00:00 2001 From: weareoutman Date: Wed, 31 Jul 2024 17:17:55 +0800 Subject: [PATCH 2/2] fix(): handle provider load error --- .../src/internal/bindListeners.spec.ts | 22 +++++--- .../runtime/src/internal/bindListeners.ts | 53 +++++++++---------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/runtime/src/internal/bindListeners.spec.ts b/packages/runtime/src/internal/bindListeners.spec.ts index c0d70e7027..9f302b9414 100644 --- a/packages/runtime/src/internal/bindListeners.spec.ts +++ b/packages/runtime/src/internal/bindListeners.spec.ts @@ -1035,8 +1035,8 @@ describe("listenerFactory for calling brick methods", () => { expect(brick.element.callbackFinally).toBeCalledWith(null); }); - test("Calling undefined method", () => { - consoleError.mockReturnValueOnce(); + test("Calling undefined method", async () => { + consoleInfo.mockReturnValueOnce(); const brick = { element: document.createElement("div"), }; @@ -1044,15 +1044,23 @@ describe("listenerFactory for calling brick methods", () => { { target: "_self", method: "callMe", + callback: { + error: { + action: "console.info", + args: ["<% EVENT.detail %>"], + }, + }, }, runtimeContext, brick )(event); - expect(consoleError).toBeCalledTimes(1); - expect(consoleError).toBeCalledWith("target has no method:", { - target: brick.element, - method: "callMe", - }); + + await (global as any).flushPromises(); + + expect(consoleInfo).toBeCalledTimes(1); + expect(consoleInfo).toBeCalledWith( + new Error("target
has no method: callMe") + ); }); }); diff --git a/packages/runtime/src/internal/bindListeners.ts b/packages/runtime/src/internal/bindListeners.ts index bc615d75a0..304f517b19 100644 --- a/packages/runtime/src/internal/bindListeners.ts +++ b/packages/runtime/src/internal/bindListeners.ts @@ -31,7 +31,7 @@ import { getTplHostElement, getTplStateStore, } from "./CustomTemplates/utils.js"; -import { handleHttpError, httpErrorToString } from "../handleHttpError.js"; +import { handleHttpError } from "../handleHttpError.js"; import { Notification } from "../Notification.js"; import { getFormStateStore } from "./FormRenderer/utils.js"; import { DataStore } from "./data/DataStore.js"; @@ -318,27 +318,21 @@ export function listenerFactory( }; } -async function handleUseProviderAction( +function handleUseProviderAction( event: Event, handler: UseProviderEventHandler, runtimeContext: RuntimeContext, runtimeBrick?: ElementHolder ) { - try { - const providerBrick = await getProviderBrick(handler.useProvider); - const method = handler.method !== "saveAs" ? "resolve" : "saveAs"; - brickCallback( - event, - providerBrick, - handler, - method, - runtimeContext, - runtimeBrick - ); - } catch (error) { - // eslint-disable-next-line no-console - console.error(httpErrorToString(error)); - } + const method = handler.method !== "saveAs" ? "resolve" : "saveAs"; + brickCallback( + event, + handler.useProvider, + handler, + method, + runtimeContext, + runtimeBrick + ); } function handleCustomAction( @@ -431,23 +425,26 @@ function handleCustomAction( async function brickCallback( event: Event, - target: HTMLElement, + targetOrProvider: HTMLElement | string, handler: ExecuteCustomBrickEventHandler | UseProviderEventHandler, method: string, runtimeContext: RuntimeContext, runtimeBrick?: ElementHolder, options?: ArgsFactoryOptions ): Promise { - if (typeof (target as any)[method] !== "function") { - // eslint-disable-next-line no-console - console.error("target has no method:", { - target, - method: method, - }); - return; - } + const isProvider = isUseProviderHandler(handler); const task = async (): Promise => { + const realTarget = isProvider + ? await getProviderBrick(targetOrProvider as string) + : (targetOrProvider as HTMLElement); + + if (typeof (realTarget as any)[method] !== "function") { + throw new Error( + `target <${realTarget.tagName.toLowerCase()}> has no method: ${method}` + ); + } + let computedArgs = argsFactory( handler.args, runtimeContext, @@ -465,7 +462,7 @@ async function brickCallback( handler.sse?.stream ); } - return (target as any)[method](...computedArgs); + return (realTarget as any)[method](...computedArgs); }; if (!handler.callback) { @@ -486,7 +483,7 @@ async function brickCallback( finally: callbackFactory("finally"), }; - if (isUseProviderHandler(handler)) { + if (isProvider) { const pollRuntimeContext = { ...runtimeContext, event,