diff --git a/js/cli/src/generate/hooks-gen.ts b/js/cli/src/generate/hooks-gen.ts index 62434823..9b5ed20a 100644 --- a/js/cli/src/generate/hooks-gen.ts +++ b/js/cli/src/generate/hooks-gen.ts @@ -22,7 +22,7 @@ export class HooksGenerator extends BaseGenerator { useProgramEvent, UseProgramParameters as useSailsProgramParameters, UseProgramQueryParameters, - UseProgramEventParameters`, + UseProgramEventParameters` ) // TODO: combine with above after hooks update .import( @@ -34,7 +34,7 @@ export class HooksGenerator extends BaseGenerator { FunctionName, QueryName, QueryReturn, - EventReturn`, + EventReturn` ) .import(`./${LIB_FILE_NAME}`, 'Program'); }; @@ -62,9 +62,9 @@ export class HooksGenerator extends BaseGenerator { .line('QueryReturn', false) .reduceIndent() .line('>,', false) - .line("'program' | 'serviceName' | 'functionName'", false) + .line("'serviceName' | 'functionName'", false) .reduceIndent() - .line('> & ProgramParameter') + .line('>') .line() .line('type UseEventParameters<', false) .increaseIndent() @@ -81,16 +81,16 @@ export class HooksGenerator extends BaseGenerator { .line('EventCallbackArgs>', false) .reduceIndent() .line('>,', false) - .line("'program' | 'serviceName' | 'functionName'", false) + .line("'serviceName' | 'functionName'", false) .reduceIndent() - .line('> & ProgramParameter') + .line('> &') .line(); }; private generateUseProgram = () => this._out .block('export function useProgram(parameters: UseProgramParameters)', () => - this._out.line('return useSailsProgram({ library: Program, ...parameters })'), + this._out.line('return useSailsProgram({ library: Program, ...parameters })') ) .line(); @@ -101,9 +101,9 @@ export class HooksGenerator extends BaseGenerator { .block(`export function ${name}({ program }: ProgramParameter)`, () => this._out.line( `return useSendProgramTransaction({ program, serviceName: '${toLowerCaseFirst( - serviceName, - )}', functionName: '${toLowerCaseFirst(functionName)}' })`, - ), + serviceName + )}', functionName: '${toLowerCaseFirst(functionName)}' })` + ) ) .line(); }; @@ -115,9 +115,9 @@ export class HooksGenerator extends BaseGenerator { .block(`export function ${name}({ program }: ProgramParameter)`, () => this._out.line( `return usePrepareProgramTransaction({ program, serviceName: '${toLowerCaseFirst( - serviceName, - )}', functionName: '${toLowerCaseFirst(functionName)}' })`, - ), + serviceName + )}', functionName: '${toLowerCaseFirst(functionName)}' })` + ) ) .line(); }; @@ -132,8 +132,8 @@ export class HooksGenerator extends BaseGenerator { `export function ${name}(parameters: UseQueryParameters<'${formattedServiceName}', '${formattedFunctionName}'>)`, () => this._out.line( - `return useProgramQuery({ ...parameters, serviceName: '${formattedServiceName}', functionName: '${formattedFunctionName}' })`, - ), + `return useProgramQuery({ ...parameters, serviceName: '${formattedServiceName}', functionName: '${formattedFunctionName}' })` + ) ) .line(); }; @@ -148,8 +148,8 @@ export class HooksGenerator extends BaseGenerator { `export function ${name}(parameters: UseEventParameters<'${formattedServiceName}', '${functionName}'>)`, () => this._out.line( - `return useProgramEvent({...parameters, serviceName: '${formattedServiceName}', functionName: '${functionName}' })`, - ), + `return useProgramEvent({...parameters, serviceName: '${formattedServiceName}', functionName: '${functionName}' })` + ) ) .line(); }; diff --git a/js/test/demo-hooks.test.tsx b/js/test/demo-hooks.test.tsx index fc1d8e1c..0d7fd9d6 100644 --- a/js/test/demo-hooks.test.tsx +++ b/js/test/demo-hooks.test.tsx @@ -1,18 +1,35 @@ import { HexString } from '@gear-js/api'; -import * as GearHooks from '@gear-js/react-hooks'; +import * as gearHooks from '@gear-js/react-hooks'; +import type { UseProgramParameters, UseProgramQueryParameters } from '@gear-js/react-hooks'; +import type { + GenericTransactionReturn, + SignAndSendOptions, + TransactionReturn, +} from '@gear-js/react-hooks/dist/esm/hooks/sails/types'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook as renderReactHook } from '@testing-library/react'; +import { renderHook as renderReactHook, waitFor } from '@testing-library/react'; import * as fs from 'fs'; import path from 'path'; import { ReactNode } from 'react'; -import { test, expect, vi, MockInstance } from 'vitest'; +import { test, expect, vi, MockInstance, expectTypeOf, describe } from 'vitest'; import { SailsIdlParser } from 'sails-js-parser'; import { toLowerCaseFirst } from 'sails-js-util'; -import { Sails } from '..'; -import { Program } from './demo/lib'; +import { ActorId, NonZeroU32, Sails } from '..'; +import { DoThatParam, Program, ReferenceCount } from './demo/lib'; +import { + useCounterAddedEvent, + useDogPositionQuery, + useDogWalkedEvent, + usePrepareCounterAddTransaction, + usePrepareReferencesIncrTransaction, + useSendDogWalkTransaction, + useSendThisThatDoThatTransaction, + useThisThatThatQuery, +} from './demo/hooks'; const { useProgram, ...demoHooks } = await import('./demo/hooks'); +const { ApiProvider } = gearHooks; const getSails = async () => { const idlPath = path.resolve(__dirname, '../../examples/demo/client/demo.idl'); @@ -22,17 +39,19 @@ const getSails = async () => { return new Sails(parser).parseIdl(idl); }; -const SAILS = await getSails(); -const QUERY_CLIENT = new QueryClient(); - -const useProgramSpy = vi.spyOn(GearHooks, 'useProgram'); -const useSendTransactionSpy = vi.spyOn(GearHooks, 'useSendProgramTransaction'); -const usePrepareTransactionSpy = vi.spyOn(GearHooks, 'usePrepareProgramTransaction'); -const useQuerySpy = vi.spyOn(GearHooks, 'useProgramQuery'); -const useEventSpy = vi.spyOn(GearHooks, 'useProgramEvent'); +const apiArgs = { endpoint: 'ws://127.0.0.1:9944' }; +const { services } = await getSails(); +const queryClient = new QueryClient(); +const useProgramSpy = vi.spyOn(gearHooks, 'useProgram'); +const useSendTransactionSpy = vi.spyOn(gearHooks, 'useSendProgramTransaction'); +const usePrepareTransactionSpy = vi.spyOn(gearHooks, 'usePrepareProgramTransaction'); +const useQuerySpy = vi.spyOn(gearHooks, 'useProgramQuery'); +const useEventSpy = vi.spyOn(gearHooks, 'useProgramEvent'); const Providers = ({ children }: { children: ReactNode }) => ( - {children} + + {children} + ); const renderHook = (hook: (initialProps: TProps) => TReturn) => @@ -43,55 +62,217 @@ const testHookParameters = ( getName: (value: string) => string, spy: MockInstance, extraArgs = {}, - getFunctionName?: (value: string) => string, + getFunctionName: (value: string) => string = (value) => value ) => { const { result } = renderHook(() => useProgram({ id: '0x01' })); const program = result.current.data; + const args = { program, ...extraArgs }; - Object.entries(SAILS.services).forEach(([serviceName, { [type]: functions }]) => { - Object.keys(functions).forEach((functionName) => { + Object.entries(services).forEach(([serviceName, service]) => + Object.keys(service[type]).forEach((functionName) => { const hookName = getName(`${serviceName}${functionName}`); const useHook: unknown = demoHooks[hookName]; if (typeof useHook !== 'function') throw new Error(`Hook ${hookName} not found`); - renderHook(() => useHook({ program, ...extraArgs })); + renderHook(() => useHook(args)); expect(spy).toHaveBeenCalledWith({ - program, + ...args, serviceName: toLowerCaseFirst(serviceName), - functionName: toLowerCaseFirst(getFunctionName ? getFunctionName(functionName) : functionName), - ...extraArgs, + functionName: toLowerCaseFirst(getFunctionName(functionName)), }); - }); - }); + }) + ); }; -test('useProgram parameters forwarding', () => { - const ARGS = { id: '0x01' as HexString, query: { enabled: true } }; - renderHook(() => useProgram(ARGS)); +describe('useProgram', () => { + test('parameters forwarding', () => { + const args = { id: '0x01' as HexString, query: { enabled: true } }; + renderHook(() => useProgram(args)); - expect(useProgramSpy).toHaveBeenCalledWith({ library: Program, ...ARGS }); + expect(useProgramSpy).toHaveBeenCalledWith({ library: Program, ...args }); + }); + + test('program instance return', async () => { + const { result } = renderHook(() => useProgram({ id: '0x01' })); + + await waitFor(() => expect(result.current.data).toBeInstanceOf(Program)); + }); + + test('parameters type', () => { + expectTypeOf(useProgram) + .parameter(0) + .toEqualTypeOf<{ id: HexString; query?: UseProgramParameters['query'] }>(); + }); }); -test('useSendTransaction parameters forwarding', () => { - testHookParameters('functions', (name) => `useSend${name}Transaction`, useSendTransactionSpy); +describe('useSendTransaction', () => { + test('parameters forwarding', () => { + testHookParameters('functions', (name) => `useSend${name}Transaction`, useSendTransactionSpy); + }); + + test('parameters type', () => { + expectTypeOf(useSendDogWalkTransaction).parameter(0).toEqualTypeOf<{ program: Program | undefined }>(); + expectTypeOf(useSendThisThatDoThatTransaction).parameter(0).toEqualTypeOf<{ program: Program | undefined }>(); + }); + + test('mutation args type', () => { + const { result: programResult } = renderHook(() => useProgram({ id: '0x01' })); + const program = programResult.current.data; + + const { result } = renderHook(() => useSendDogWalkTransaction({ program })); + const { sendTransaction } = result.current; + + const { result: anotherResult } = renderHook(() => useSendThisThatDoThatTransaction({ program })); + const { sendTransaction: anotherSendTransaction } = anotherResult.current; + + expectTypeOf(sendTransaction) + .parameter(0) + .extract>() + .toMatchTypeOf<{ args: [number, number] }>(); + + expectTypeOf(anotherSendTransaction) + .parameter(0) + .extract>() + .toMatchTypeOf<{ args: [DoThatParam] }>(); + }); + + test('mutation return type', () => { + const { result: programResult } = renderHook(() => useProgram({ id: '0x01' })); + const program = programResult.current.data; + + const { result } = renderHook(() => useSendDogWalkTransaction({ program: programResult.current.data })); + const { sendTransactionAsync } = result.current; + + const { result: anotherResult } = renderHook(() => useSendThisThatDoThatTransaction({ program })); + const { sendTransactionAsync: anotherSendTransactionAsync } = anotherResult.current; + + expectTypeOf(sendTransactionAsync) + .returns.resolves.pick('awaited') + .toMatchTypeOf<{ awaited: { response: null } }>(); + + expectTypeOf(anotherSendTransactionAsync) + .returns.resolves.pick('awaited') + .toMatchTypeOf<{ awaited: { response: { ok: [ActorId, NonZeroU32] } | { err: [string] } } }>(); + }); }); -test('usePrepareTransaction parameters forwarding', () => { - testHookParameters('functions', (name) => `usePrepare${name}Transaction`, usePrepareTransactionSpy); +describe('usePrepareTransaction', () => { + test('parameters forwarding', () => { + testHookParameters('functions', (name) => `usePrepare${name}Transaction`, usePrepareTransactionSpy); + }); + + test('parameters type', () => { + expectTypeOf(usePrepareCounterAddTransaction).parameter(0).toEqualTypeOf<{ program: Program | undefined }>(); + expectTypeOf(usePrepareReferencesIncrTransaction).parameter(0).toEqualTypeOf<{ program: Program | undefined }>(); + }); + + test('mutation args type', () => { + const { result: programResult } = renderHook(() => useProgram({ id: '0x01' })); + const program = programResult.current.data; + + const { result } = renderHook(() => usePrepareCounterAddTransaction({ program })); + const { prepareTransaction } = result.current; + + const { result: anotherResult } = renderHook(() => usePrepareReferencesIncrTransaction({ program })); + const { prepareTransaction: anotherPrepareTransaction } = anotherResult.current; + + expectTypeOf(prepareTransaction).parameter(0).toMatchTypeOf<{ args: [number] }>(); + expectTypeOf(anotherPrepareTransaction).parameter(0).toMatchTypeOf<{ args: [] }>(); + }); + + test('mutation return type', async () => { + const { result: programResult } = renderHook(() => useProgram({ id: '0x01' })); + const program = programResult.current.data; + + const { result } = renderHook(() => usePrepareCounterAddTransaction({ program })); + const { prepareTransactionAsync } = result.current; + + const { result: anotherResult } = renderHook(() => usePrepareReferencesIncrTransaction({ program })); + const { prepareTransactionAsync: anotherPrepareTransactionAsync } = anotherResult.current; + + expectTypeOf(prepareTransactionAsync) + .returns.resolves.pick('transaction') + .toMatchTypeOf<{ transaction: TransactionReturn<(value: number) => GenericTransactionReturn> }>(); + + expectTypeOf(anotherPrepareTransactionAsync) + .returns.resolves.pick('transaction') + .toMatchTypeOf<{ transaction: TransactionReturn<() => GenericTransactionReturn> }>(); + }); }); -test('useQuery parameters forwarding', () => { - testHookParameters('queries', (name) => `use${name}Query`, useQuerySpy, { query: { enabled: true } }); +describe('useQuery', () => { + test('parameters forwarding', () => { + testHookParameters('queries', (name) => `use${name}Query`, useQuerySpy, { query: { enabled: true } }); + }); + + test('parameters type', () => { + expectTypeOf(useDogPositionQuery) + .parameter(0) + .toEqualTypeOf< + Omit< + UseProgramQueryParameters< + Program, + 'dog', + 'position', + [originAddress?: string, value?: string | number | bigint, atBlock?: HexString], + [number, number] + >, + 'serviceName' | 'functionName' + > + >(); + + expectTypeOf(useThisThatThatQuery) + .parameter(0) + .toEqualTypeOf< + Omit< + UseProgramQueryParameters< + Program, + 'thisThat', + 'that', + [originAddress?: string, value?: string | number | bigint, atBlock?: HexString], + { ok: string } | { err: string } + >, + 'serviceName' | 'functionName' + > + >(); + }); + + test('query return type', () => { + const { result: programResult } = renderHook(() => useProgram({ id: '0x01' })); + const program = programResult.current.data; + + const { result } = renderHook(() => useDogPositionQuery({ program, args: [] })); + const { data } = result.current; + + const { result: anotherResult } = renderHook(() => useThisThatThatQuery({ program, args: [] })); + const { data: anotherData } = anotherResult.current; + + expectTypeOf(data).toMatchTypeOf<[number, number]>(); + expectTypeOf(anotherData).toMatchTypeOf<{ ok: string } | { err: string }>(); + }); }); -test('useEvent parameters forwarding', () => { - testHookParameters( - 'events', - (name) => `use${name}Event`, - useEventSpy, - { query: { enabled: true }, onData: () => {} }, - (name) => `subscribeTo${name}Event`, - ); +describe('useEvent', () => { + test('parameters forwarding', () => { + testHookParameters( + 'events', + (name) => `use${name}Event`, + useEventSpy, + { query: { enabled: true } }, + (name) => `subscribeTo${name}Event` + ); + }); + + test('parameters + onData args type', () => { + expectTypeOf(useCounterAddedEvent) + .parameter(0) + .toEqualTypeOf<{ program: Program | undefined; onData: (value: number) => void }>(); + + expectTypeOf(useDogWalkedEvent).parameter(0).toEqualTypeOf<{ + program: Program | undefined; + onData: (value: { from: [number, number]; to: [number, number] }) => void; + }>(); + }); }); diff --git a/js/vitest.config.ts b/js/vitest.config.ts index 5830487f..fdff97f5 100644 --- a/js/vitest.config.ts +++ b/js/vitest.config.ts @@ -1,18 +1,17 @@ import { defineConfig } from 'vitest/config'; +const HOOKS_REGEX = '**/*-hooks.test.ts?(x)'; + export default defineConfig({ // resolving manually cuz vitest is using nodejs resolution, remove after hooks update // https://github.com/vitest-dev/vitest/discussions/4233 - resolve: { - alias: { - '@gear-js/react-hooks': '@gear-js/react-hooks/dist/esm/index.mjs', - }, - }, + resolve: { alias: { '@gear-js/react-hooks': '@gear-js/react-hooks/dist/esm/index.mjs' } }, test: { - include: ['**/*-hooks.test.ts?(x)'], // targeting only hooks + include: [HOOKS_REGEX], // targeting only hooks environment: 'happy-dom', // faster than jsdom watch: false, // fire one time server: { deps: { inline: ['@gear-js/react-hooks'] } }, // patching esm to allow spying + typecheck: { enabled: true, include: [HOOKS_REGEX], ignoreSourceErrors: true }, // test types }, });