@furystack/inject
Version:
Core FuryStack package
382 lines • 16.7 kB
JavaScript
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