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..9b1ca3d5 100644 --- a/packages/lib/src/hooks/use-app-event.test.tsx +++ b/packages/lib/src/hooks/use-app-event.test.tsx @@ -1,14 +1,16 @@ -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, - * 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. */ @@ -113,4 +115,32 @@ 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); + + // 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]); + }); }); 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(() => {