jeffijoe/awilix

View on GitHub
src/__tests__/container.test.ts

Summary

Maintainability
F
1 wk
Test Coverage
import { throws } from 'smid'
import * as util from 'util'
import { createContainer, AwilixContainer } from '../container'
import { Lifetime } from '../lifetime'
import { AwilixResolutionError } from '../errors'
import { aliasTo, asClass, asFunction, asValue } from '../resolvers'
import { InjectionMode } from '../injection-mode'

class Test {
  repo: Repo
  constructor({ repo }: any) {
    this.repo = repo
  }

  stuff() {
    return this.repo.getStuff()
  }
}

class Repo {
  getStuff() {
    return 'stuff'
  }
}

class ManualTest {
  repo: Repo
  constructor(repo: Repo) {
    this.repo = repo
  }
}

describe('createContainer', () => {
  it('returns an object', () => {
    const container = createContainer()
    expect(typeof container).toBe('object')
  })
})

describe('container', () => {
  it('lets me register something and resolve it', () => {
    const container = createContainer()
    container.register({ someValue: asValue(42) })
    container.register({
      test: asFunction((deps: any) => {
        return {
          someValue: deps.someValue,
        }
      }),
    })

    const test = container.cradle.test
    expect(test).toBeTruthy()
    expect(test.someValue).toBe(42)
  })

  it('lets me register something and resolve it via classic injection mode', () => {
    const container = createContainer({
      injectionMode: InjectionMode.CLASSIC,
    })
    container.register({
      manual: asClass(ManualTest),
      repo: asClass(Repo),
    })

    const test = container.cradle.manual
    expect(test).toBeTruthy()
    expect(test.repo).toBeTruthy()
  })

  describe('register', () => {
    it('supports multiple registrations in a single call', () => {
      const container = createContainer()
      container.register({
        universe: asValue(42),
        leet: asValue(1337),
      })

      container.register({
        service: asFunction(({ func, universe }: any) => ({
          method: () => func(universe),
        })),
        func: asFunction(
          () => (answer: any) => 'Hello world, the answer is ' + answer,
        ),
      })

      expect(Object.keys(container.registrations).length).toBe(4)

      expect(container.resolve<any>('service').method()).toBe(
        'Hello world, the answer is 42',
      )
    })

    it('supports classes', () => {
      const container = createContainer()
      container.register({
        test: asClass(Test),
        repo: asClass(Repo),
      })

      expect(container.resolve<Test>('test').stuff()).toBe('stuff')
    })
  })

  describe('has', () => {
    it('returns true if the registration does exist', () => {
      const container = createContainer()
      container.register({ theValue: asValue('theValue') })

      expect(container.hasRegistration('theValue')).toBe(true)
    })

    it('returns false if the registration does not exist', () => {
      expect(createContainer().hasRegistration('theValue')).toBe(false)
    })
  })

  describe('resolve', () => {
    it('resolves the dependency graph and supports all resolvers', () => {
      class TestClass {
        factoryResult: any
        constructor({ factory }: any) {
          this.factoryResult = factory()
        }
      }

      const factorySpy = jest.fn((cradle) => 'factory ' + cradle.value)
      const container = createContainer()
      container.register({ value: asValue(42) })
      container.register({
        factory: asFunction((cradle: any) => () => factorySpy(cradle)),
      })
      container.register({ theClass: asClass(TestClass) })

      const root = container.resolve<TestClass>('theClass')
      expect(root.factoryResult).toBe('factory 42')
    })

    it('throws an AwilixResolutionError when there are unregistered dependencies', () => {
      const container = createContainer()
      const err = throws(() => container.resolve('nope'))
      expect(err).toBeInstanceOf(AwilixResolutionError)
      expect(err.message).toMatch(/nope/i)
    })

    it('throws an AwilixResolutionError that supports symbols', () => {
      const container = createContainer()
      const S = Symbol('i am the derg')
      const err = throws(() => container.resolve(S))
      expect(err).toBeInstanceOf(AwilixResolutionError)
      expect(err.message).toMatch(/i am the derg/i)
    })

    it('throws an AwilixResolutionError that supports symbols on multiple levels', () => {
      const container = createContainer()
      const S1 = Symbol('i am the derg')
      const S2 = Symbol('i am not the derg')
      container.register({ [S2]: asFunction(({ [S1]: theDerg }) => theDerg) })
      const err = throws(() => container.resolve(S2))
      expect(err).toBeInstanceOf(AwilixResolutionError)
      expect(err.message).toMatch(/i am the derg/i)
    })

    it('throws an AwilixResolutionError with a resolution path when resolving an unregistered dependency', () => {
      const container = createContainer()
      container.register({
        first: asFunction((cradle: any) => cradle.second),
        second: asFunction((cradle: any) => cradle.third),
        third: asFunction((cradle: any) => cradle.unregistered),
      })

      const err = throws(() => container.resolve('first'))
      expect(err.message).toContain('first -> second -> third')
    })

    it('does not screw up the resolution stack when called twice', () => {
      const container = createContainer()
      container.register({
        first: asFunction((cradle: any) => cradle.second),
        otherFirst: asFunction((cradle: any) => cradle.second),
        second: asFunction((cradle: any) => cradle.third),
        third: asFunction((cradle: any) => cradle.unregistered),
      })

      const err1 = throws(() => container.resolve('first'))
      const err2 = throws(() => container.resolve('otherFirst'))
      expect(err1.message).toContain('first -> second -> third')
      expect(err2.message).toContain('otherFirst -> second -> third')
    })

    it('supports transient lifetime', () => {
      const container = createContainer()
      let counter = 1
      container.register({
        hehe: asFunction(() => counter++).transient(),
      })

      expect(container.cradle.hehe).toBe(1)
      expect(container.cradle.hehe).toBe(2)
    })

    it('supports singleton lifetime', () => {
      const container = createContainer()
      let counter = 1
      container.register({
        hehe: asFunction(() => counter++).singleton(),
      })

      expect(container.cradle.hehe).toBe(1)
      expect(container.cradle.hehe).toBe(1)
    })

    it('supports scoped lifetime', () => {
      const container = createContainer()
      let scopedCounter = 1
      container.register({
        scoped: asFunction(() => scopedCounter++).scoped(),
      })

      const scope1 = container.createScope()
      expect(scope1.cradle.scoped).toBe(1)
      expect(scope1.cradle.scoped).toBe(1)

      const scope2 = container.createScope()
      expect(scope2.cradle.scoped).toBe(2)
      expect(scope2.cradle.scoped).toBe(2)
    })

    it('caches singletons regardless of scope', () => {
      const container = createContainer()
      let singletonCounter = 1
      container.register({
        singleton: asFunction(() => singletonCounter++).singleton(),
      })

      const scope1 = container.createScope()
      expect(scope1.cradle.singleton).toBe(1)
      expect(scope1.cradle.singleton).toBe(1)

      const scope2 = container.createScope()
      expect(scope2.cradle.singleton).toBe(1)
      expect(scope2.cradle.singleton).toBe(1)
    })

    it('resolves transients regardless of scope', () => {
      const container = createContainer()
      let transientCounter = 1
      container.register({
        transient: asFunction(() => transientCounter++).transient(),
      })

      const scope1 = container.createScope()
      expect(scope1.cradle.transient).toBe(1)
      expect(scope1.cradle.transient).toBe(2)

      const scope2 = container.createScope()
      expect(scope2.cradle.transient).toBe(3)
      expect(scope2.cradle.transient).toBe(4)
    })

    it('does not use parents cache when scoped', () => {
      const container = createContainer()
      let scopedCounter = 1
      container.register({
        scoped: asFunction(() => scopedCounter++).scoped(),
      })

      const scope1 = container.createScope()
      expect(scope1.cradle.scoped).toBe(1)
      expect(scope1.cradle.scoped).toBe(1)

      const scope2 = scope1.createScope()
      expect(scope2.cradle.scoped).toBe(2)
      expect(scope2.cradle.scoped).toBe(2)

      expect(container.cradle.scoped).toBe(3)
      expect(container.cradle.scoped).toBe(3)
      expect(scope2.cradle.scoped).toBe(2)
      expect(scope1.cradle.scoped).toBe(1)
    })

    it('supports the readme example of scopes', () => {
      // Increments the counter every time it is resolved.
      const container = createContainer()
      let counter = 1
      container.register({
        counterValue: asFunction(() => counter++).scoped(),
      })
      const scope1 = container.createScope()
      const scope2 = container.createScope()

      const scope1Child = scope1.createScope()

      expect(scope1.cradle.counterValue === 1).toBe(true)
      expect(scope1.cradle.counterValue === 1).toBe(true)
      expect(scope2.cradle.counterValue === 2).toBe(true)
      expect(scope2.cradle.counterValue === 2).toBe(true)

      expect(scope1Child.cradle.counterValue === 3).toBe(true)
      // assert that the parent scope was not affected
      expect(scope1.cradle.counterValue === 1).toBe(true)
    })

    it('supports nested scopes', () => {
      const container = createContainer()

      // Increments the counter every time it is resolved.
      let counter = 1
      container.register({
        counterValue: asFunction(() => counter++).scoped(),
      })
      const scope1 = container.createScope()
      const scope2 = container.createScope()

      const scope1Child = scope1.createScope()

      expect(scope1.cradle.counterValue).toBe(1)
      expect(scope1.cradle.counterValue).toBe(1)
      expect(scope2.cradle.counterValue).toBe(2)
      expect(scope2.cradle.counterValue).toBe(2)
      expect(scope1Child.cradle.counterValue).toBe(3)
    })

    it('resolves dependencies in scope', () => {
      const container = createContainer()
      // Register a transient function
      // that returns the value of the scope-provided dependency.
      // For this example we could also use scoped lifetime.
      container.register({
        scopedValue: asFunction((cradle: any) => 'Hello ' + cradle.someValue),
      })

      // Create a scope and register a value.
      const scope = container.createScope()
      scope.register({
        someValue: asValue('scope'),
      })

      expect(scope.cradle.scopedValue).toBe('Hello scope')
    })

    it('cannot find a scope-registered value when resolved from root', () => {
      const container = createContainer()
      // Register a transient function
      // that returns the value of the scope-provided dependency.
      // For this example we could also use scoped lifetime.
      container.register({
        scopedValue: asFunction((cradle: any) => 'Hello ' + cradle.someValue),
      })

      // Create a scope and register a value.
      const scope = container.createScope()
      scope.register({
        someValue: asValue('scope'),
      })

      expect(() => container.cradle.scopedValue).toThrowError(
        AwilixResolutionError,
      )
    })

    it('supports overwriting values in a scope', () => {
      const container = createContainer()
      // It does not matter when the scope is created,
      // it will still have anything that is registered
      // in it's parent.
      const scope = container.createScope()

      container.register({
        value: asValue('root'),
        usedValue: asFunction((cradle: any) => cradle.value),
      })

      scope.register({
        value: asValue('scope'),
      })

      expect(container.cradle.usedValue).toBe('root')
      expect(scope.cradle.usedValue).toBe('scope')
    })

    it('supports default parameters in CLASSIC mode', () => {
      const container = createContainer({
        injectionMode: InjectionMode.CLASSIC,
      })

      class ClassWithDefaults {
        method(p1 = 123) {
          /**/
        }

        // tslint:disable-next-line:member-ordering
        constructor(
          public val: any,
          public fn: any,
        ) {
          /**/
        }
      }

      const fnWithDefaults =
        (val = 456, nope = 'nope') =>
        (p: string) =>
          p + nope + val

      container.register({
        val: asValue(123),
        cls: asClass(ClassWithDefaults),
        fn: asFunction(fnWithDefaults),
      })

      expect(container.resolve<ClassWithDefaults>('cls').val).toBe(123)
      expect(container.resolve<ClassWithDefaults>('cls').fn('yep')).toBe(
        'yepnope123',
      )
    })

    it('throws an AwilixResolutionError when there are cyclic dependencies', () => {
      const container = createContainer()
      container.register({
        first: asFunction((cradle: any) => cradle.second),
        second: asFunction((cradle: any) => cradle.third),
        third: asFunction((cradle: any) => cradle.second),
      })

      const err = throws(() => container.resolve('first'))
      expect(err.message).toContain('first -> second -> third -> second')
    })

    it('throws an AwilixResolutionError when the lifetime is unknown', () => {
      const container = createContainer()
      container.register({
        first: asFunction((cradle: any) => cradle.second),
        second: asFunction(() => 'hah', { lifetime: 'lol' as any }),
      })

      const err = throws(() => container.resolve('first'))
      expect(err.message).toContain('first -> second')
      expect(err.message).toContain('lol')
    })

    it('behaves properly when the cradle is returned from an async function', async () => {
      const container = createContainer()
      container.register({ value: asValue(42) })

      async function example() {
        return container.cradle
      }

      const { value } = await example()

      expect(value).toBe(42)
    })
  })

  describe('loadModules', () => {
    let container: AwilixContainer
    beforeEach(() => {
      container = createContainer()
    })

    it('returns the container', () => {
      expect(container.loadModules([])).toBe(container)
    })

    it('returns a Promise of the container if used with esModules true', async () => {
      expect(await container.loadModules([], { esModules: true })).toBe(
        container,
      )
    })
  })

  describe('setting a property on the cradle', () => {
    it('should fail', () => {
      expect(() => {
        createContainer().cradle.lol = 'nope'
      }).toThrowError(Error)
    })
  })

  describe('using util.inspect on the container', () => {
    it('should return a summary', () => {
      const container = createContainer().register({
        val1: asValue(1),
        val2: asValue(2),
        fn1: asFunction(() => true),
        c1: asClass(Repo),
      })

      expect(util.inspect(container)).toBe(
        '[AwilixContainer (registrations: 4)]',
      )
      expect(
        util.inspect(container.createScope().register({ val3: asValue(3) })),
      ).toBe('[AwilixContainer (scoped, registrations: 5)]')

      expect(container.resolve('inspect')).toBeInstanceOf(Function)
      expect(container.resolve(util.inspect.custom)).toBeInstanceOf(Function)
    })
  })

  describe('using util.inspect on the cradle', () => {
    it('should return the preconfigured string', () => {
      const container = createContainer().register({
        val1: asValue(1),
        val2: asValue(2),
        fn1: asFunction(() => true),
        c1: asClass(Repo),
      })

      expect(util.inspect(container.cradle)).toBe(
        '[object AwilixContainerCradle]',
      )
    })
  })

  describe('resolving reserved names', () => {
    it('returns the createContainer function for constructor', () => {
      const container = createContainer()
      expect(container.resolve('constructor')).toBe(createContainer)
    })
  })

  describe('using Array.from on the cradle', () => {
    it('should return an Array with registration names', () => {
      const container = createContainer().register({
        val1: asValue(1),
        val2: asValue(2),
        fn1: asFunction(() => true),
        c1: asClass(Repo),
      })

      expect(Array.from(container.cradle)).toEqual([
        'val1',
        'val2',
        'fn1',
        'c1',
      ])
    })

    it('should return injector keys as well', () => {
      class KeysTest {
        keys: Array<string>
        constructor(cradle: any) {
          this.keys = Array.from(cradle)
        }
      }
      const container = createContainer().register({
        val1: asValue(1),
        val2: asValue(2),
        test: asClass(KeysTest).inject(() => ({ injected: true })),
      })

      const result = container.resolve<KeysTest>('test')
      expect(result.keys).toEqual(['val1', 'val2', 'test', 'injected'])
    })
  })

  describe('explicitly trying to fuck shit up', () => {
    it('should prevent you from fucking shit up', () => {
      const container = createContainer({
        injectionMode: null as any,
      }).register({
        answer: asValue(42),
        theAnswer: asFunction(
          ({ answer }: any) =>
            () =>
              answer,
        ),
      })

      const theAnswer = container.resolve<() => number>('theAnswer')
      expect(theAnswer()).toBe(42)
    })

    it('should default to PROXY injection mode when unknown', () => {
      const container = createContainer({
        injectionMode: 'I dunno maaaang...' as any,
      }).register({
        answer: asValue(42),
        theAnswer: asFunction(
          ({ answer }: any) =>
            () =>
              answer,
        ),
      })

      const theAnswer = container.resolve<() => number>('theAnswer')
      expect(theAnswer()).toBe(42)
    })
  })

  describe('strict mode', () => {
    describe('lifetime mismatch check', () => {
      it('allows longer lifetime modules to depend on shorter lifetime dependencies by default', () => {
        const container = createContainer()
        container.register({
          first: asFunction((cradle: any) => cradle.second, {
            lifetime: Lifetime.SCOPED,
          }),
          second: asFunction(() => 'hah'),
        })

        expect(container.resolve('first')).toBe('hah')
      })

      it('throws an AwilixResolutionError when longer lifetime modules depend on shorter lifetime dependencies and strict is set', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          first: asFunction((cradle: any) => cradle.second, {
            lifetime: Lifetime.SCOPED,
          }),
          second: asFunction(() => 'hah'),
        })

        const err = throws(() => container.resolve('first'))
        expect(err.message).toContain('first -> second')
        expect(err.message).toContain(
          "Dependency 'second' has a shorter lifetime than its ancestor: 'first'",
        )
      })

      it('does not throw an error when an injector proxy is used and strict is set', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          first: asFunction((cradle: any) => cradle.injected, {
            lifetime: Lifetime.SCOPED,
          }).inject(() => ({ injected: 'hah' })),
          injected: asFunction(() => 'foobar'),
        })

        expect(container.resolve('first')).toBe('hah')
      })

      it('allows for asValue() to be used when strict is set', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          first: asFunction((cradle: any) => cradle.val, {
            lifetime: Lifetime.SCOPED,
          }),
          second: asFunction((cradle: any) => cradle.secondVal, {
            lifetime: Lifetime.SINGLETON,
          }),
          val: asValue('hah'),
          secondVal: asValue('foobar'),
        })

        expect(container.resolve('first')).toBe('hah')
        expect(container.resolve('second')).toBe('foobar')
      })

      it('allows aliasTo to be used when strict is set', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          first: asFunction((cradle: any) => cradle.second, {
            lifetime: Lifetime.SINGLETON,
          }),
          second: aliasTo('val'),
          val: asValue('hah'),
        })

        expect(container.resolve('first')).toBe('hah')
      })

      it('detects when an aliasTo resolution violates lifetime constraints', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          first: asFunction((cradle: any) => cradle.second, {
            lifetime: Lifetime.SCOPED,
          }),
          second: aliasTo('val'),
          val: asValue('hah'),
        })
        const scope = container.createScope()
        scope.register({
          val: asFunction(() => 'foobar'),
        })

        const err = throws(() => scope.resolve('first'))
        expect(err.message).toContain('first -> second -> val')
        expect(err.message).toContain(
          "Dependency 'val' has a shorter lifetime than its ancestor: 'first'",
        )
      })
    })

    describe('singleton resolution using only root container', () => {
      it('resolves singletons using root container only, even if called from scope', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          scoped: asFunction((cradle: any) => cradle.val, {
            lifetime: Lifetime.SCOPED,
          }),
          singleton: asFunction((cradle: any) => cradle.val, {
            lifetime: Lifetime.SINGLETON,
          }),
        })
        const scope = container.createScope()

        let err = throws(() => scope.resolve('scoped'))
        expect(err).toBeInstanceOf(AwilixResolutionError)
        expect(err.message).toContain('scoped -> val')

        scope.register({
          val: asValue('foobar'),
        })

        expect(scope.resolve('scoped')).toBe('foobar')

        err = throws(() => scope.resolve('singleton'))
        expect(err).toBeInstanceOf(AwilixResolutionError)
        expect(err.message).toContain('singleton -> val')

        container.register({
          val: asValue('hah'),
        })

        expect(scope.resolve('singleton')).toBe('hah')
      })

      it('preserves the resolution stack when resolving a singleton using a parent container, from a scope', () => {
        const container = createContainer({
          strict: true,
        })
        container.register({
          scoped: asFunction((cradle: any) => cradle.singleton, {
            lifetime: Lifetime.SCOPED,
          }),
          singleton: asFunction((cradle: any) => cradle.val, {
            lifetime: Lifetime.SINGLETON,
          }),
        })
        const scope = container.createScope()

        const err = throws(() => scope.resolve('scoped'))
        expect(err).toBeInstanceOf(AwilixResolutionError)
        expect(err.message).toContain('scoped -> singleton -> val')
      })
    })

    describe('singleton registration on scope check', () => {
      it('detects and errors when a singleton is registered on a scope', () => {
        const container = createContainer({
          strict: true,
        })
        const scope = container.createScope()
        const err = throws(() =>
          scope.register({
            test: asFunction(() => 42, { lifetime: Lifetime.SINGLETON }),
          }),
        )
        expect(err.message).toContain("Could not register 'test'")
        expect(err.message).toContain(
          'Cannot register a singleton on a scoped container',
        )
      })
    })
  })
})

describe('setting a name on the registration options', () => {
  it('should not work', () => {
    const container = createContainer().register({
      test: asFunction(() => 42, { lifetime: Lifetime.SCOPED, name: 'lol' }),
    })

    expect(container.resolve('test')).toBe(42)
    expect(container.registrations.lol).toBe(undefined)
  })
})

describe('registering and resolving symbols', () => {
  it('works', () => {
    const S1 = Symbol('test 1')
    const S2 = Symbol('test 2')
    const container = createContainer()
      .register({
        [S1]: asValue(42),
      })
      .register(S2, asValue(24))

    expect(container.resolve(S1)).toBe(42)
    expect(container.cradle[S1]).toBe(42)

    expect(container.resolve(S2)).toBe(24)
    expect(container.cradle[S2]).toBe(24)
  })
})

describe('spreading the cradle', () => {
  it('does not throw', () => {
    const container = createContainer().register({
      val1: asValue(1),
      val2: asValue(2),
    })
    expect([...container.cradle]).toEqual(['val1', 'val2'])
  })
})

describe('using Object.keys() on the cradle', () => {
  it('should return the registration keys', () => {
    const container = createContainer().register({
      val1: asValue(1),
      val2: asValue(2),
    })
    expect(Object.keys(container.cradle)).toEqual(['val1', 'val2'])
  })

  it('should return injector keys', () => {
    class KeysTest {
      keys: Array<string>
      constructor(cradle: any) {
        this.keys = Object.keys(cradle)
      }
    }
    const container = createContainer().register({
      val1: asValue(1),
      val2: asValue(2),
      test: asClass(KeysTest).inject(() => ({ injected: true, val2: 10 })),
    })

    const result = container.resolve<KeysTest>('test')
    expect(result.keys).toEqual(['val1', 'val2', 'test', 'injected'])
  })
})

describe('using Object.getOwnPropertyDescriptor with injector proxy', () => {
  it('returns expected values', () => {
    class KeysTest {
      nonexistentProp: PropertyDescriptor | undefined
      testProp: PropertyDescriptor | undefined
      constructor(cradle: any) {
        this.testProp = Object.getOwnPropertyDescriptor(cradle, 'test')
        this.nonexistentProp = Object.getOwnPropertyDescriptor(
          cradle,
          'nonexistent',
        )
      }
    }
    const container = createContainer()
      .register({ val1: asValue(1), val2: asValue(2) })
      .register({
        test: asClass(KeysTest).inject(() => ({ injected: true })),
      })

    const result = container.resolve<KeysTest>('test')
    expect(result.testProp).toBeDefined()
    expect(result.nonexistentProp).toBeFalsy()
  })
})

describe('using Object.getOwnPropertyDescriptor with container cradle', () => {
  it('returns expected values', () => {
    class KeysTest {
      nonexistentProp: PropertyDescriptor | undefined
      testProp: PropertyDescriptor | undefined
      constructor(cradle: any) {
        this.testProp = Object.getOwnPropertyDescriptor(cradle, 'test')
        this.nonexistentProp = Object.getOwnPropertyDescriptor(
          cradle,
          'nonexistent',
        )
      }
    }
    const container = createContainer()
      .register({ val1: asValue(1), val2: asValue(2) })
      .register({
        test: asClass(KeysTest),
      })

    const result = container.resolve<KeysTest>('test')
    expect(result.testProp).toBeDefined()
    expect(result.nonexistentProp).toBeFalsy()
  })
})

describe('memoizing registrations', () => {
  it('should not cause issues', () => {
    const container = createContainer().register({ val1: asValue(123) })

    const scope1 = container.createScope()
    const scope2 = scope1.createScope()

    expect(scope1.resolve('val1')).toBe(123)
    expect(scope2.resolve('val1')).toBe(123)

    container.register({ val2: asValue(321) })
    expect(scope2.resolve('val2')).toBe(321)
    expect(scope1.resolve('val2')).toBe(321)

    container.register({ val3: asValue(1337) }).register({
      keys: asFunction((cradle: any) => Object.keys(cradle)).inject(() => ({
        injected: true,
      })),
    })
    expect(scope2.resolve('keys')).toEqual([
      'val1',
      'val2',
      'val3',
      'keys',
      'injected',
    ])
  })

  describe('build', () => {
    const fn = (val: any) => val
    const container = createContainer().register({ val: asValue(1337) })

    class BuildTest {
      val: any
      constructor({ val }: any) {
        this.val = val
      }
    }

    it('throws when the target is falsy', () => {
      expect(() => createContainer().build(null!)).toThrowError(/null/)
      expect(() => createContainer().build(undefined!)).toThrowError(
        /undefined/,
      )
      expect(() => createContainer().build({} as any)).toThrowError(/object/)
    })

    it('returns resolved value when passed a resolver', () => {
      expect(container.build(asFunction(fn).classic())).toBe(1337)
      expect(container.build(asClass(BuildTest).proxy())).toBeInstanceOf(
        BuildTest,
      )
      expect(container.build(asClass(BuildTest).proxy()).val).toBe(1337)
    })

    it('returns resolved value when passed a function', () => {
      expect(
        container.build(fn, { injectionMode: InjectionMode.CLASSIC }),
      ).toBe(1337)
    })

    it('returns resolved value when passed a class', () => {
      expect(container.build(BuildTest)).toBeInstanceOf(BuildTest)
      expect(container.build(BuildTest).val).toBe(1337)
    })

    it('uses containers injection mode by default', () => {
      const otherContainer = createContainer({
        injectionMode: InjectionMode.CLASSIC,
      })
      otherContainer.register({ val: asValue(1337) })
      expect(otherContainer.build(fn)).toBe(1337)
    })
  })

  describe('well-known names and symbols', () => {
    it('should have toJSON() return [object AwilixContainerCradle]', () => {
      expect(createContainer().cradle.toJSON()).toBe(
        '[object AwilixContainerCradle]',
      )
    })

    it('JSON.stringify() should return [object AwilixContainerCradle]', () => {
      expect(JSON.stringify(createContainer().cradle)).toBe(
        '"[object AwilixContainerCradle]"',
      )
    })

    it('Symbol.toStringTag', () => {
      const result = Object.prototype.toString.call(createContainer().cradle)
      expect(result).toBe('[object AwilixContainerCradle]')
    })
  })
})