import 'reflect-metadata' ; import { Container , injectable , interfaces , unmanaged } from 'inversify' ; import * as React from 'react' ; import { useState } from 'react' ; import * as renderer from 'react-test-renderer' ; import { assert , IsExact } from 'conditional-type-checks' ; import * as hooksModule from '../src/hooks' ; import { Provider , useAllInjections , useContainer , useInjection , useOptionalInjection , } from '../src' ; function staticTypecheckOnly ( _fn ) { return ( ) => { } ; } function throwErr ( msg ) { throw new Error ( msg ) ; } @ injectable ( ) class Foo { readonly name = 'foo' ; } @ injectable ( ) class Bar { readonly name : string ; constructor ( @ unmanaged ( ) tag ) { this . name = 'bar-' + tag ; } } const aTag = 'a-tag' ; const bTag = 'b-tag' ; const multiId = Symbol ( 'multi-id' ) ; class OptionalService { readonly label = 'OptionalService' as const ; } const RootComponent = ( { children } ) => { const [ container ] = useState ( ( ) => { const c = new Container ( ) ; c . bind ( Foo ) . toSelf ( ) ; c . bind ( Bar ) . toDynamicValue ( ( ) => new Bar ( 'a' ) ) . whenTargetNamed ( aTag ) ; c . bind ( Bar ) . toDynamicValue ( ( ) => new Bar ( 'a' ) ) . whenTargetTagged ( aTag , 'a' ) ; c . bind ( Bar ) . toDynamicValue ( ( ) => new Bar ( 'b' ) ) . whenTargetNamed ( bTag ) ; c . bind ( multiId ) . toConstantValue ( 'x' ) ; c . bind ( multiId ) . toConstantValue ( 'y' ) ; c . bind ( multiId ) . toConstantValue ( 'z' ) ; return c ; } ) ; return ( < Provider container = { container } > < div > { children } < /div> < /Provider>); } ; describe ( 'useContainer hook' , ( ) => { const hookSpy = jest . spyOn ( hooksModule , 'useContainer' ) ; const ChildComponent = ( ) => { const resolvedContainer = useContainer ( ) ; return < div > { resolvedContainer . id } < /div>; } ; afterEach ( ( ) => { hookSpy . mockClear ( ) ; } ) ; test ( 'types' , staticTypecheckOnly ( ( ) => { const container = useContainer ( ) ; assert < IsExact < typeof container , interfaces . Container > > ( true ) ; const valueResolvedFromContainer = useContainer ( c => { assert < IsExact < typeof c , interfaces . Container > > ( true ) ; return c . resolve ( Foo ) ; } ) ; assert < IsExact < typeof valueResolvedFromContainer , Foo > > ( true ) ; } ) ) ; test ( 'resolves container from context' , ( ) => { const container = new Container ( ) ; const tree = renderer . create ( < Provider container = { container } > < ChildComponent /> < /Provider>).toJSON(); expect ( hookSpy ) . toHaveBeenCalledTimes ( 1 ) ; expect ( hookSpy ) . lastReturnedWith ( container ) ; expect ( tree . type ) . toBe ( 'div' ) ; expect ( tree . children [ 0 ] ) . toEqual ( `${ container . id }` ) ; } ) ; test ( 'throws when no context found (missing Provider)' , ( ) => { expect ( ( ) => { renderer . create ( < ChildComponent />); } ) . toThrowError ( 'Cannot find Inversify container on React Context. `Provider` component is missing in component tree.' ) ; expect ( hookSpy ) . toHaveBeenCalled ( ) ; expect ( hookSpy ) . toHaveReturnedTimes ( 0 ) ; } ) ; } ) ; describe ( 'useInjection hook' , ( ) => { test ( 'resolves using service identifier (newable)' , ( ) => { const ChildComponent = ( ) => { const foo = useInjection ( Foo ) ; return < div > { foo . name } < /div>; } ; const tree = renderer . create ( < RootComponent > < ChildComponent /> < /RootComponent>).toJSON(); expect ( tree . type ) . toBe ( 'div' ) ; expect ( tree . children [ 0 ] . type ) . toBe ( 'div' ) ; expect ( tree . children [ 0 ] . children ) . toEqual ( [ 'foo' ] ) ; } ) ; test ( 'resolves using service identifier (string)' , ( ) => { const container = new Container ( ) ; container . bind ( 'FooFoo' ) . to ( Foo ) ; const ChildComponent = ( ) => { const foo = useInjection < Foo > ( 'FooFoo' ) ; return < div > { foo . name } < /div>; } ; const tree = renderer . create ( < Provider container = { container } > < ChildComponent /> < /Provider>).toJSON(); expect ( tree . type ) . toBe ( 'div' ) ; expect ( tree . children ) . toEqual ( [ 'foo' ] ) ; } ) ; test ( 'resolves using service identifier (symbol)' , ( ) => { const identifier = Symbol ( 'Foo' ) as interfaces . ServiceIdentifier < Foo > ; const container = new Container ( ) ; container . bind ( identifier ) . to ( Foo ) ; const ChildComponent = ( ) => { const foo = useInjection ( identifier ) ; return < div > { foo . name } < /div>; } ; const tree = renderer . create ( < Provider container = { container } > < ChildComponent /> < /Provider>).toJSON(); expect ( tree . type ) . toBe ( 'div' ) ; expect ( tree . children ) . toEqual ( [ 'foo' ] ) ; } ) ; } ) ; describe ( 'useOptionalInjection hook' , ( ) => { const hookSpy = jest . spyOn ( hooksModule , 'useOptionalInjection' ) ; afterEach ( ( ) => { hookSpy . mockClear ( ) ; } ) ; test ( 'types' , staticTypecheckOnly ( ( ) => { const opt = useOptionalInjection ( Foo ) ; assert < IsExact < typeof opt , Foo | undefined > > ( true ) ; const optWithDefault = useOptionalInjection ( Foo , ( ) => 'default' as const ) ; assert < IsExact < typeof optWithDefault , Foo | 'default' > > ( true ) ; } ) ) ; test ( 'returns undefined for missing injection/binding' , ( ) => { const ChildComponent = ( ) => { const optionalThing = useOptionalInjection ( OptionalService ) ; return ( < > { optionalThing === undefined ? 'missing' : throwErr ( 'unexpected' ) } < />); } ; const tree = renderer . create ( < RootComponent > < ChildComponent /> < /RootComponent>).toJSON(); expect ( hookSpy ) . toHaveBeenCalledTimes ( 1 ) ; expect ( hookSpy ) . toHaveReturnedWith ( undefined ) ; expect ( tree . children ) . toEqual ( [ 'missing' ] ) ; } ) ; test ( 'resolves using fallback to default value' , ( ) => { const defaultThing = { label : 'myDefault' , isMyDefault : true , } as const ; const ChildComponent = ( ) => { const defaultFromOptional = useOptionalInjection ( OptionalService , ( ) => defaultThing ) ; if ( defaultFromOptional instanceof OptionalService ) { throwErr ( 'unexpected' ) ; } else { assert < IsExact < typeof defaultFromOptional , typeof defaultThing > > ( true ) ; expect ( defaultFromOptional ) . toBe ( defaultThing ) ; } return ( < > { defaultFromOptional . label } < />); } ; const tree = renderer . create ( < RootComponent > < ChildComponent /> < /RootComponent>).toJSON(); expect ( hookSpy ) . toHaveBeenCalledTimes ( 1 ) ; expect ( hookSpy ) . toHaveReturnedWith ( defaultThing ) ; expect ( tree . children ) . toEqual ( [ defaultThing . label ] ) ; } ) ; test ( 'resolves if injection/binding exists' , ( ) => { const ChildComponent = ( ) => { const foo = useOptionalInjection ( Foo ) ; return ( < > { foo !== undefined ? foo . name : throwErr ( 'Cannot resolve injection for Foo' ) } < />); } ; const tree = renderer . create ( < RootComponent > < ChildComponent /> < /RootComponent>).toJSON(); expect ( hookSpy ) . toHaveBeenCalledTimes ( 1 ) ; expect ( tree . children ) . toEqual ( [ 'foo' ] ) ; } ) ; } ) ; describe ( 'useAllInjections hook' , ( ) => { const hookSpy = jest . spyOn ( hooksModule , 'useAllInjections' ) ; afterEach ( ( ) => { hookSpy . mockClear ( ) ; } ) ; test ( 'resolves all injections' , ( ) => { const ChildComponent = ( ) => { const stuff = useAllInjections ( multiId ) ; return ( < > { stuff . join ( ',' ) } < />); } ; const tree = renderer . create ( < RootComponent > < ChildComponent /> < /RootComponent>).toJSON(); expect ( hookSpy ) . toHaveBeenCalledTimes ( 1 ) ; expect ( tree . children ) . toEqual ( [ 'x,y,z' ] ) ; } ) ; } ) ;