diff --git a/packages/core/src/Signal.ts b/packages/core/src/Signal.ts index a18b86cf5a..e337e03d9d 100644 --- a/packages/core/src/Signal.ts +++ b/packages/core/src/Signal.ts @@ -20,9 +20,11 @@ export class Signal { on(fn: (...args: T) => void, target?: any): void; /** * Add a structured binding listener. Structured bindings support clone remapping. + * The target method will be invoked as `method(...signalArgs, ...args)` — + * runtime signal arguments come first, bound arguments are appended. * @param target - The target component * @param methodName - The method name to invoke on the target - * @param args - Pre-resolved arguments + * @param args - Pre-resolved arguments appended after the runtime signal arguments */ on(target: Component, methodName: string, ...args: any[]): void; on(fnOrTarget: ((...args: T) => void) | Component, targetOrMethodName?: any, ...args: any[]): void { @@ -37,9 +39,11 @@ export class Signal { once(fn: (...args: T) => void, target?: any): void; /** * Add a one-time structured binding listener. + * The target method will be invoked as `method(...signalArgs, ...args)` — + * runtime signal arguments come first, bound arguments are appended. * @param target - The target component * @param methodName - The method name to invoke on the target - * @param args - Pre-resolved arguments + * @param args - Pre-resolved arguments appended after the runtime signal arguments */ once(target: Component, methodName: string, ...args: any[]): void; once(fnOrTarget: ((...args: T) => void) | Component, targetOrMethodName?: any, ...args: any[]): void { @@ -171,7 +175,7 @@ export class Signal { const methodName = targetOrMethodName as string; const fn = args.length > 0 - ? (...signalArgs: any[]) => (target as any)[methodName](...args, ...signalArgs) + ? (...signalArgs: any[]) => (target as any)[methodName](...signalArgs, ...args) : (...signalArgs: any[]) => (target as any)[methodName](...signalArgs); this._listeners.push({ fn: fn as (...args: T) => void, diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/CloneUtils.test.ts index 79fb05fdfa..8cf3dd9d55 100644 --- a/tests/src/core/CloneUtils.test.ts +++ b/tests/src/core/CloneUtils.test.ts @@ -1,12 +1,4 @@ -import { - Entity, - MeshRenderer, - Script, - Signal, - assignmentClone, - deepClone, - ignoreClone -} from "@galacean/engine-core"; +import { Entity, MeshRenderer, Script, Signal, assignmentClone, deepClone, ignoreClone } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { describe, expect, it } from "vitest"; @@ -108,7 +100,7 @@ class ClickHandler extends Script { this.callCount++; } - handleClickWithPrefix(prefix: string): void { + handleClickWithPrefix(arg: number, prefix: string): void { this.callCount++; this.lastPrefix = prefix; } @@ -553,7 +545,9 @@ describe("Clone remap", async () => { const parent = rootEntity.createChild("parent"); const script = parent.addComponent(SignalScript); let called = false; - script.onFire.on(() => { called = true; }); + script.onFire.on(() => { + called = true; + }); const cloned = parent.clone(); const cs = cloned.getComponent(SignalScript); diff --git a/tests/src/core/Signal.test.ts b/tests/src/core/Signal.test.ts index cbf399a731..82a4fef4f4 100644 --- a/tests/src/core/Signal.test.ts +++ b/tests/src/core/Signal.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from "vitest"; class TestHandler extends Script { callCount = 0; lastPrefix = ""; + lastArgs: any[] = []; handleClick() { this.callCount++; @@ -14,6 +15,11 @@ class TestHandler extends Script { this.callCount++; this.lastPrefix = prefix; } + + handleWithArgs(...args: any[]) { + this.callCount++; + this.lastArgs = args; + } } describe("Signal", async () => { @@ -274,6 +280,56 @@ describe("Signal", async () => { expect(fn2).toHaveBeenCalledOnce(); }); + // ---- Structured binding arg order (runtime first, bound last) ---- + + it("structured binding: runtime args precede bound args", () => { + const signal = new Signal<[string, number]>(); + const entity = root.createChild("sb-order"); + const handler = entity.addComponent(TestHandler); + + signal.on(handler, "handleWithArgs", "boundA", "boundB"); + signal.invoke("event", 42); + expect(handler.lastArgs).toEqual(["event", 42, "boundA", "boundB"]); + + entity.destroy(); + }); + + it("structured binding: event object stays at index 0 regardless of bound args count", () => { + const event = { type: "click", x: 10, y: 20 }; + const signal = new Signal<[typeof event]>(); + const e1 = root.createChild("sb-evt-1"); + const e2 = root.createChild("sb-evt-2"); + const h1 = e1.addComponent(TestHandler); + const h2 = e2.addComponent(TestHandler); + + signal.on(h1, "handleWithArgs"); + signal.on(h2, "handleWithArgs", "ctx", 1, true); + + signal.invoke(event); + + expect(h1.lastArgs[0]).toBe(event); + expect(h2.lastArgs[0]).toBe(event); + expect(h2.lastArgs).toEqual([event, "ctx", 1, true]); + + e1.destroy(); + e2.destroy(); + }); + + it("structured binding once: runtime + bound args order preserved", () => { + const signal = new Signal<[number]>(); + const entity = root.createChild("sb-once-args"); + const handler = entity.addComponent(TestHandler); + + signal.once(handler, "handleWithArgs", "bound"); + signal.invoke(99); + signal.invoke(100); + + expect(handler.callCount).toBe(1); + expect(handler.lastArgs).toEqual([99, "bound"]); + + entity.destroy(); + }); + // ---- Clone ---- it("clone: closure-based listeners are not cloned", () => { diff --git a/tests/src/ui/UIInteractive.test.ts b/tests/src/ui/UIInteractive.test.ts index 646cd19e3a..0a11923bae 100644 --- a/tests/src/ui/UIInteractive.test.ts +++ b/tests/src/ui/UIInteractive.test.ts @@ -1,7 +1,16 @@ import { Camera, PointerEventData, Script, SpriteDrawMode } from "@galacean/engine-core"; import { Color, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; -import { Button, ColorTransition, Image, ScaleTransition, Text, UICanvas, UIGroup, UITransform } from "@galacean/engine-ui"; +import { + Button, + ColorTransition, + Image, + ScaleTransition, + Text, + UICanvas, + UIGroup, + UITransform +} from "@galacean/engine-ui"; import { describe, expect, it, vi } from "vitest"; class ClickHandler extends Script { @@ -12,7 +21,7 @@ class ClickHandler extends Script { this.callCount++; } - handleClickWithPrefix(prefix: string) { + handleClickWithPrefix(event: PointerEventData, prefix: string) { this.callCount++; this.lastPrefix = prefix; } @@ -40,7 +49,7 @@ describe("Button", async () => { const canvasEntity = root.createChild("canvas"); canvasEntity.addComponent(UIGroup); - const commonTextEntity = canvasEntity.createChild("commonText") + const commonTextEntity = canvasEntity.createChild("commonText"); const commonText = commonTextEntity.addComponent(Text); // Create button @@ -55,7 +64,6 @@ describe("Button", async () => { text.color.set(0, 0, 0, 1); const button = buttonEntity.addComponent(Button); - it("Set and Get", () => { // Click let clickCount = 0; @@ -135,7 +143,7 @@ describe("Button", async () => { const cloneButton = cloneButtonEntity.getComponent(Button); const cloneTransitions = cloneButton.transitions; const cloneTransition = cloneTransitions[0]; - expect(cloneTransition.target).to.eq(cloneButtonEntity.getComponent(Image)) + expect(cloneTransition.target).to.eq(cloneButtonEntity.getComponent(Image)); const cloneTransitionOne = cloneTransitions[1]; expect(cloneTransitionOne.target).to.eq(cloneButtonEntity.findByName("Text").getComponent(Text));