From 2ed0d6f62c4bbbbb689db4a5e38dcd08cde2b80e Mon Sep 17 00:00:00 2001 From: JonasPfi Date: Sun, 16 Aug 2026 23:14:11 +0200 Subject: [PATCH 1/2] fix(hooks): forward all arguments in useAppEvent for custom events --- .changeset/deep-towns-attend.md | 5 ++++ packages/lib/src/hooks/use-app-event.test.tsx | 30 ++++++++++++++++++- packages/lib/src/hooks/use-app-event.ts | 9 ++---- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 .changeset/deep-towns-attend.md diff --git a/.changeset/deep-towns-attend.md b/.changeset/deep-towns-attend.md new file mode 100644 index 00000000..91977490 --- /dev/null +++ b/.changeset/deep-towns-attend.md @@ -0,0 +1,5 @@ +--- +'@playcanvas/react': patch +--- + +Fix useAppEvent dropping arguments for custom events diff --git a/packages/lib/src/hooks/use-app-event.test.tsx b/packages/lib/src/hooks/use-app-event.test.tsx index a2ef761b..b0e49a33 100644 --- a/packages/lib/src/hooks/use-app-event.test.tsx +++ b/packages/lib/src/hooks/use-app-event.test.tsx @@ -1,10 +1,11 @@ -import { renderHook } from '@testing-library/react'; +import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { Application } from '../Application.tsx'; import { useAppEvent } from './use-app-event.ts'; +import { useApp } from './use-app.tsx'; /** * Note that we can't test the actual firing of the callbacks in tests, @@ -113,4 +114,31 @@ describe('useAppEvent', () => { // Should not throw during cleanup unmount(); }); + it('should forward all arguments to the callback for custom events', async () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve the interface generic coverage + interface CustomEventMap { + levelComplete: (level: number, score: number) => void; + } + + const levelCompleteCallback = vi.fn(); + let capturedApp: ReturnType | undefined; + + renderHook( + () => { + capturedApp = useApp(); + useAppEvent('levelComplete', levelCompleteCallback); + }, + { + wrapper: ({ children }) => {children} + } + ); + + await waitFor(() => expect(capturedApp).toBeDefined()); + + capturedApp!.fire('levelComplete', 3, 1000); + + const [level, score] = levelCompleteCallback.mock.calls[0]; + expect(level).toBe(3); + expect(score).toBe(1000); + }); }); diff --git a/packages/lib/src/hooks/use-app-event.ts b/packages/lib/src/hooks/use-app-event.ts index ff2c61e8..53216c53 100644 --- a/packages/lib/src/hooks/use-app-event.ts +++ b/packages/lib/src/hooks/use-app-event.ts @@ -81,14 +81,9 @@ export function useAppEvent { - if (event === 'update') { - // update event always receives delta time as first argument - (callback as (dt: number) => void)(args[0] as number); - } else { - (callback as () => void)(); - } + (callback as (...args: unknown[]) => void)(...args); }, - [callback, event] + [callback] ); useEffect(() => { From 8248d54416403a230a69b1c1cf2c962a7ab17031 Mon Sep 17 00:00:00 2001 From: abstrakt Date: Fri, 25 Sep 2026 12:06:18 +0100 Subject: [PATCH 2/2] test(hooks): tighten useAppEvent argument-forwarding test Assert the callback fired once before inspecting its arguments, compare only the leading args since PlayCanvas pads fire() to eight, and update the header note now that custom events can be fired in tests. --- packages/lib/src/hooks/use-app-event.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/use-app-event.test.tsx b/packages/lib/src/hooks/use-app-event.test.tsx index b0e49a33..9b1ca3d5 100644 --- a/packages/lib/src/hooks/use-app-event.test.tsx +++ b/packages/lib/src/hooks/use-app-event.test.tsx @@ -8,8 +8,9 @@ import { useAppEvent } from './use-app-event.ts'; import { useApp } from './use-app.tsx'; /** - * Note that we can't test the actual firing of the callbacks in tests, - * because these events are not fire in the Null device type. + * Note that we can't test the actual firing of the built-in frame events in tests, + * because these events are not fired in the Null device type. Custom events can be + * fired directly with `app.fire(...)`. * * It's possible that we can run headless once we have a node-wgpu environment. */ @@ -114,6 +115,7 @@ describe('useAppEvent', () => { // Should not throw during cleanup unmount(); }); + it('should forward all arguments to the callback for custom events', async () => { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- preserve the interface generic coverage interface CustomEventMap { @@ -137,8 +139,8 @@ describe('useAppEvent', () => { capturedApp!.fire('levelComplete', 3, 1000); - const [level, score] = levelCompleteCallback.mock.calls[0]; - expect(level).toBe(3); - expect(score).toBe(1000); + // PlayCanvas pads unused fire() arguments with undefined, so compare the leading ones + expect(levelCompleteCallback).toHaveBeenCalledTimes(1); + expect(levelCompleteCallback.mock.calls[0].slice(0, 2)).toEqual([3, 1000]); }); });