UNPKG

@furystack/inject

Version:
382 lines 16.7 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; import { usingAsync } from '@furystack/utils'; import { describe, expect, it, vi } from 'vitest'; import { Injectable } from './injectable.js'; import { Injected } from './injected.js'; import { Injector } from './injector.js'; describe('Injector', () => { it('Shold be constructed', () => { const i = new Injector(); expect(i).toBeInstanceOf(Injector); }); it('Should be disposed', async () => { await usingAsync(new Injector(), async () => { /** */ }); }); it('Parent should be undefined by default', () => { const i = new Injector(); expect(i.options.parent).toBeUndefined(); }); it('Should throw an error when setting an Injector instance', async () => { await usingAsync(new Injector(), async (i) => { expect(() => i.setExplicitInstance(new Injector())).toThrowError('Cannot set an injector instance as injectable'); }); }); it('Should throw an error when trying to set an instance without decorator', async () => { await usingAsync(new Injector(), async (i) => { class TestClass { } expect(() => i.setExplicitInstance(new TestClass())).toThrowError(`The class 'TestClass' is not an injectable`); }); }); describe('Transient lifetime', () => { it('Should not store an instance in the cache', async () => { await usingAsync(new Injector(), async (i) => { let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'transient' }) ], InstanceClass); const instance = i.getInstance(InstanceClass); expect(instance).toBeInstanceOf(InstanceClass); expect(i.cachedSingletons.get(InstanceClass)).toBeUndefined(); }); }); it('Should throw an error if you try to set an explicit instance', async () => { await usingAsync(new Injector(), async (i) => { let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'transient' }) ], InstanceClass); const instance = new InstanceClass(); expect(() => i.setExplicitInstance(instance)).toThrowError(`Cannot set an instance of 'InstanceClass' as it's lifetime is set to 'transient'`); }); }); }); describe('Scoped lifetime', () => { it('Should set and return instance from cache', () => { const i = new Injector(); let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'scoped' }) ], InstanceClass); const instance = new InstanceClass(); i.setExplicitInstance(instance); expect(i.getInstance(InstanceClass)).toBe(instance); }); it('Should instantiate and return an instance', () => { const i = new Injector(); let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'scoped' }) ], InstanceClass); expect(i.getInstance(InstanceClass)).toBeInstanceOf(InstanceClass); }); it('Scoped with transient dependencies should throw an error', async () => { let Tr2 = class Tr2 { }; Tr2 = __decorate([ Injectable({ lifetime: 'transient' }) ], Tr2); let Sc2 = class Sc2 { }; __decorate([ Injected(Tr2), __metadata("design:type", Tr2) ], Sc2.prototype, "sc", void 0); Sc2 = __decorate([ Injectable({ lifetime: 'scoped' }) ], Sc2); await usingAsync(new Injector(), async (i) => { expect(() => i.getInstance(Sc2)).toThrowError(`Injector error: Scoped type 'Sc2' depends on transient injectables: Tr2:transient`); }); }); }); describe('Singleton lifetime', () => { it('Should return from a parent injector if available', () => { const parent = new Injector(); const i = parent.createChild(); let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'singleton' }) ], InstanceClass); const instance = new InstanceClass(); parent.setExplicitInstance(instance); expect(i.getInstance(InstanceClass)).toBe(instance); expect(parent.cachedSingletons.get(InstanceClass)).toBe(instance); }); it('Should create instance on a parent injector if not available', () => { const parent = new Injector(); const i = parent.createChild(); let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'singleton' }) ], InstanceClass); expect(i.getInstance(InstanceClass)).toBeInstanceOf(InstanceClass); expect(parent.cachedSingletons.get(InstanceClass)).toBeInstanceOf(InstanceClass); }); it('Singleton with transient dependencies should throw an error', async () => { let Trs1 = class Trs1 { }; Trs1 = __decorate([ Injectable({ lifetime: 'transient' }) ], Trs1); let St1 = class St1 { }; __decorate([ Injected(Trs1), __metadata("design:type", Trs1) ], St1.prototype, "lt", void 0); St1 = __decorate([ Injectable({ lifetime: 'singleton' }) ], St1); await usingAsync(new Injector(), async (i) => { expect(() => i.getInstance(St1)).toThrowError(`Injector error: Singleton type 'St1' depends on non-singleton injectables: Trs1:transient`); }); }); it('Singleton with transient dependencies should throw an error', async () => { let Sc1 = class Sc1 { }; Sc1 = __decorate([ Injectable({ lifetime: 'scoped' }) ], Sc1); let St2 = class St2 { }; __decorate([ Injected(Sc1), __metadata("design:type", Sc1) ], St2.prototype, "sc", void 0); St2 = __decorate([ Injectable({ lifetime: 'singleton' }) ], St2); await usingAsync(new Injector(), async (i) => { expect(() => i.getInstance(St2)).toThrowError(`Injector error: Singleton type 'St2' depends on non-singleton injectables: Sc1:scoped`); }); }); }); describe('Explicit lifetime', () => { it('Should return the instance from the cache', async () => { await usingAsync(new Injector(), async (i) => { let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'explicit' }) ], InstanceClass); const instance = new InstanceClass(); i.setExplicitInstance(instance); expect(i.getInstance(InstanceClass)).toBe(instance); }); }); it('Should return an instance from the parent injector', async () => { await usingAsync(new Injector(), async (i) => { const child = i.createChild(); let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'explicit' }) ], InstanceClass); const instance = new InstanceClass(); i.setExplicitInstance(instance); expect(child.getInstance(InstanceClass)).toBe(instance); }); }); it('Should throw an error if the instance is not set', async () => { await usingAsync(new Injector(), async (i) => { let InstanceClass = class InstanceClass { }; InstanceClass = __decorate([ Injectable({ lifetime: 'explicit' }) ], InstanceClass); expect(() => i.getInstance(InstanceClass)).toThrowError(`Cannot instantiate an instance of 'InstanceClass' as it's lifetime is set to 'explicit'. Ensure to initialize it properly`); }); }); }); it('Should resolve injectable fields', () => { const i = new Injector(); let Injected1 = class Injected1 { }; Injected1 = __decorate([ Injectable() ], Injected1); let Injected2 = class Injected2 { }; Injected2 = __decorate([ Injectable() ], Injected2); let InstanceClass = class InstanceClass { }; __decorate([ Injected(Injected1), __metadata("design:type", Injected1) ], InstanceClass.prototype, "injected1", void 0); __decorate([ Injected(Injected2), __metadata("design:type", Injected2) ], InstanceClass.prototype, "injected2", void 0); InstanceClass = __decorate([ Injectable() ], InstanceClass); const instance = i.getInstance(InstanceClass); expect(instance).toBeInstanceOf(InstanceClass); expect(instance.injected1).toBeInstanceOf(Injected1); expect(instance.injected2).toBeInstanceOf(Injected2); }); it('Should resolve injectable fields recursively', () => { const i = new Injector(); let Injected1 = class Injected1 { }; Injected1 = __decorate([ Injectable() ], Injected1); let Injected2 = class Injected2 { }; __decorate([ Injected(Injected1), __metadata("design:type", Injected1) ], Injected2.prototype, "injected1", void 0); Injected2 = __decorate([ Injectable() ], Injected2); let InstanceClass = class InstanceClass { }; __decorate([ Injected(Injected2), __metadata("design:type", Injected2) ], InstanceClass.prototype, "injected2", void 0); InstanceClass = __decorate([ Injectable() ], InstanceClass); expect(i.getInstance(InstanceClass)).toBeInstanceOf(InstanceClass); expect(i.getInstance(InstanceClass).injected2.injected1).toBeInstanceOf(Injected1); }); it('Should throw if failed to dispose one or more entries', async () => { expect.assertions(1); let TestDisposableThrows = class TestDisposableThrows { [Symbol.dispose]() { throw Error(':('); } }; TestDisposableThrows = __decorate([ Injectable({ lifetime: 'singleton' }) ], TestDisposableThrows); const i = new Injector(); i.getInstance(TestDisposableThrows); await expect(async () => await i[Symbol.asyncDispose]()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: There was an error during disposing '1' global disposable objects: Error: :(]`); }); it('Should throw if failed to dispose async one or more entries', async () => { expect.assertions(1); let TestDisposableThrows = class TestDisposableThrows { async [Symbol.asyncDispose]() { throw Error(':('); } }; TestDisposableThrows = __decorate([ Injectable({ lifetime: 'singleton' }) ], TestDisposableThrows); const i = new Injector(); i.getInstance(TestDisposableThrows); await expect(async () => await i[Symbol.asyncDispose]()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: There was an error during disposing '1' global disposable objects: Error: :(]`); }); it('Should dispose cached entries on dispose and tolerate non-disposable ones', async () => { const doneCallback = vi.fn(); let TestDisposable = class TestDisposable { [Symbol.dispose]() { doneCallback(); } }; TestDisposable = __decorate([ Injectable({ lifetime: 'explicit' }) ], TestDisposable); let TestInstance = class TestInstance { }; TestInstance = __decorate([ Injectable({ lifetime: 'explicit' }) ], TestInstance); await usingAsync(new Injector(), async (i) => { i.setExplicitInstance(new TestDisposable()); i.setExplicitInstance(new TestInstance()); }); expect(doneCallback).toBeCalledTimes(1); }); it('Remove should remove an entity from the cached instance list', async () => { await usingAsync(new Injector(), async (i) => { let InjectableClass = class InjectableClass { }; InjectableClass = __decorate([ Injectable({ lifetime: 'scoped' }) ], InjectableClass); i.setExplicitInstance({}, InjectableClass); i.remove(InjectableClass); expect(i.cachedSingletons.size).toBe(0); }); }); it('Requesting an Injector instance should return self', async () => { await usingAsync(new Injector(), async (i) => { expect(i.getInstance(Injector)).toBe(i); }); }); it('Requesting an undecorated instance should throw an error', async () => { class UndecoratedTestClass { } await usingAsync(new Injector(), async (i) => { expect(() => i.getInstance(UndecoratedTestClass)).toThrowError(`The class 'UndecoratedTestClass' is not an injectable`); }); }); it('Should exec an init() method, if present', async () => { let InitClass = class InitClass { initWasCalled = false; init() { this.initWasCalled = true; } }; InitClass = __decorate([ Injectable() ], InitClass); await usingAsync(new Injector(), async (i) => { const instance = i.getInstance(InitClass); expect(instance.initWasCalled).toBe(true); }); }); describe('Disposed injector', () => { it('Should throw an error on getInstance', async () => { const i = new Injector(); await i[Symbol.asyncDispose](); expect(() => i.getInstance(Injector)).toThrowError('Injector already disposed'); }); it('Should throw an error on setExplicitInstance', async () => { const i = new Injector(); await i[Symbol.asyncDispose](); expect(() => i.setExplicitInstance({})).toThrowError('Injector already disposed'); }); it('Should throw an error on remove', async () => { const i = new Injector(); await i[Symbol.asyncDispose](); expect(() => i.remove(Object)).toThrowError('Injector already disposed'); }); it('Should throw an error on createChild', async () => { const i = new Injector(); await i[Symbol.asyncDispose](); expect(() => i.createChild()).toThrowError('Injector already disposed'); }); it('Should throw an error on dispose', async () => { const i = new Injector(); await i[Symbol.asyncDispose](); await expect(async () => await i[Symbol.asyncDispose]()).rejects.toThrowError('Injector already disposed'); }); }); }); //# sourceMappingURL=injector.spec.js.map