packages/__tests__/src/1-kernel/di.get.spec.ts
import { all, Constructable, DI, factory, IContainer, resolve, IResolvedFactory, lazy, newInstanceForScope, newInstanceOf, optional, Registration, singleton, transient } from '@aurelia/kernel';
import { assert } from '@aurelia/testing';
describe('1-kernel/di.get.spec.ts', function () {
let container: IContainer;
beforeEach(function () {
container = DI.createContainer();
});
afterEach(function () {
assert.throws(() => resolve(class Abc{}));
});
describe('@lazy', function () {
class Bar {
}
class Foo {
public readonly provider: () => Bar = resolve(lazy(Bar));
}
it('@singleton', function () {
const bar0 = container.get(Foo).provider();
const bar1 = container.get(Foo).provider();
assert.strictEqual(bar0, bar1);
});
it('@transient', function () {
container.register(Registration.transient(Bar, Bar));
const bar0 = container.get(Foo).provider();
const bar1 = container.get(Foo).provider();
assert.notStrictEqual(bar0, bar1);
});
});
describe('@scoped', function () {
describe("true", function () {
@singleton({ scoped: true })
class ScopedFoo { }
describe('Foo', function () {
const constructor = ScopedFoo;
it('children', function () {
const root = DI.createContainer();
const child1 = root.createChild();
const child2 = root.createChild();
const a = child1.get(constructor);
const b = child2.get(constructor);
const c = child1.get(constructor);
assert.strictEqual(a, c, 'a and c are the same');
assert.notStrictEqual(a, b, 'a and b are not the same');
assert.strictEqual(root.has(constructor, false), false, 'root has class');
assert.strictEqual(child1.has(constructor, false), true, 'child1 has class');
assert.strictEqual(child2.has(constructor, false), true, 'child2 has class');
});
it('root', function () {
const root = DI.createContainer();
const child1 = root.createChild();
const child2 = root.createChild();
const a = root.get(constructor);
const b = child2.get(constructor);
const c = child1.get(constructor);
assert.strictEqual(a, c, 'a and c are the same');
assert.strictEqual(a, b, 'a and b are the same');
assert.strictEqual(root.has(constructor, false), true, 'root has class');
assert.strictEqual(child1.has(constructor, false), false, 'child1 does not have class');
assert.strictEqual(child2.has(constructor, false), false, 'child2 does not have class');
});
});
});
describe('false', function () {
@singleton({ scoped: false })
class ScopedFoo { }
describe('Foo', function () {
const constructor = ScopedFoo;
it('children', function () {
const root = DI.createContainer();
const child1 = root.createChild();
const child2 = root.createChild();
const a = child1.get(constructor);
const b = child2.get(constructor);
const c = child1.get(constructor);
assert.strictEqual(a, c, 'a and c are the same');
assert.strictEqual(a, b, 'a and b are the same');
assert.strictEqual(root.has(constructor, false), true, 'root has class');
assert.strictEqual(child1.has(constructor, false), false, 'child1 has class');
assert.strictEqual(child2.has(constructor, false), false, 'child2 has class');
});
});
describe('default', function () {
@singleton
class DefaultFoo { }
const constructor = DefaultFoo;
it('children', function () {
const root = DI.createContainer();
const child1 = root.createChild();
const child2 = root.createChild();
const a = child1.get(constructor);
const b = child2.get(constructor);
const c = child1.get(constructor);
assert.strictEqual(a, c, 'a and c are the same');
assert.strictEqual(a, b, 'a and b are the same');
assert.strictEqual(root.has(constructor, false), true, 'root has class');
assert.strictEqual(child1.has(constructor, false), false, 'child1 has class');
assert.strictEqual(child2.has(constructor, false), false, 'child2 has class');
});
});
});
});
describe('@optional', function () {
it('with default', function () {
class Foo {
public readonly test: string = resolve(optional('key')) ?? 'hello';
}
assert.strictEqual(container.get(Foo).test, 'hello');
});
it('no default, but param allows undefined', function () {
class Foo {
public readonly test?: string = resolve(optional('key'));
}
assert.strictEqual(container.get(Foo).test, undefined);
});
it('no default, param does not allow undefind', function () {
class Foo {
public readonly test: string = resolve(optional('key'))!;
}
assert.strictEqual(container.get(Foo).test, undefined);
});
it('interface with default', function () {
const Strings = DI.createInterface<string[]>(x => x.instance([]));
class Foo {
public readonly test: string[] = resolve(optional(Strings))!;
}
assert.deepStrictEqual(container.get(Foo).test, undefined);
});
it('interface with default and default in constructor', function () {
const MyStr = DI.createInterface<string>(x => x.instance('hello'));
class Foo {
public readonly test: string = resolve(optional(MyStr)) ?? 'test';
}
assert.deepStrictEqual(container.get(Foo).test, 'test');
});
it('interface with default registered and default in constructor', function () {
const MyStr = DI.createInterface<string>(x => x.instance('hello'));
container.register(MyStr);
class Foo {
public readonly test: string = resolve(optional(MyStr)) ?? 'test';
}
assert.deepStrictEqual(container.get(Foo).test, 'hello');
});
});
describe('intrinsic', function () {
// TODO: Enable those tests once the decorator metadata is emitted by TS.
// The tests are disabled because TS with TC39 decorators (non-legacy), does not emit the decorator metadata as of now.
// The following tests are dependent on that and hence cannot be successfully run.
// Refer: https://github.com/microsoft/TypeScript/issues/55788
describe.skip('bad', function () {
it('Array', function () {
@singleton
class Foo {
public constructor(private readonly test: string[]) {
}
}
assert.throws(() => console.log(container.get(Foo)));
});
it('ArrayBuffer', function () {
@singleton
class Foo {
public constructor(private readonly test: ArrayBuffer) {
}
}
assert.throws(() => container.get(Foo));
});
it('Boolean', function () {
@singleton
class Foo {
public constructor(private readonly test: Boolean) {
}
}
assert.throws(() => container.get(Foo));
});
it('boolean', function () {
@singleton
class Foo {
public constructor(private readonly test: boolean) {
}
}
assert.throws(() => container.get(Foo));
});
it('DataView', function () {
@singleton
class Foo {
public constructor(private readonly test: DataView) {
}
}
assert.throws(() => container.get(Foo));
});
it('Date', function () {
@singleton
class Foo {
public constructor(private readonly test: Date) {
}
}
assert.throws(() => container.get(Foo));
});
it('Error', function () {
@singleton
class Foo {
public constructor(private readonly test: Error) {
}
}
assert.throws(() => container.get(Foo));
});
it('EvalError', function () {
@singleton
class Foo {
public constructor(private readonly test: EvalError) {
}
}
assert.throws(() => container.get(Foo));
});
it('Float32Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Float32Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Float64Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Float64Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Function', function () {
@singleton
class Foo {
public constructor(private readonly test: Function) {
}
}
assert.throws(() => container.get(Foo));
});
it('Int8Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Int8Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Int16Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Int16Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Int32Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Int16Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Map', function () {
@singleton
class Foo {
public constructor(private readonly test: Map<unknown, unknown>) {
}
}
assert.throws(() => container.get(Foo));
});
it('Number', function () {
@singleton
class Foo {
public constructor(private readonly test: Number) {
}
}
assert.throws(() => container.get(Foo));
});
it('number', function () {
@singleton
class Foo {
public constructor(private readonly test: number) {
}
}
assert.throws(() => container.get(Foo));
});
it('Object', function () {
@singleton
class Foo {
public constructor(private readonly test: Object) {
}
}
assert.throws(() => container.get(Foo));
});
it('object', function () {
@singleton
class Foo {
public constructor(private readonly test: object) {
}
}
assert.throws(() => container.get(Foo));
});
it('Promise', function () {
@singleton
class Foo {
public constructor(private readonly test: Promise<unknown>) {
}
}
assert.throws(() => container.get(Foo));
});
it('RangeError', function () {
@singleton
class Foo {
public constructor(private readonly test: RangeError) {
}
}
assert.throws(() => container.get(Foo));
});
it('ReferenceError', function () {
@singleton
class Foo {
public constructor(private readonly test: ReferenceError) {
}
}
assert.throws(() => container.get(Foo));
});
it('RegExp', function () {
@singleton
class Foo {
public constructor(private readonly test: RegExp) {
}
}
assert.throws(() => container.get(Foo));
});
it('Set', function () {
@singleton
class Foo {
public constructor(private readonly test: Set<unknown>) {
}
}
assert.throws(() => container.get(Foo));
});
if (typeof SharedArrayBuffer !== 'undefined') {
it('SharedArrayBuffer', function () {
@singleton
class Foo {
public constructor(private readonly test: SharedArrayBuffer) {
}
}
assert.throws(() => container.get(Foo));
});
}
it('String', function () {
@singleton
class Foo {
public constructor(private readonly test: String) {
}
}
assert.throws(() => container.get(Foo));
});
it('string', function () {
@singleton
class Foo {
public constructor(private readonly test: string) {
}
}
assert.throws(() => container.get(Foo));
});
it('SyntaxError', function () {
@singleton
class Foo {
public constructor(private readonly test: SyntaxError) {
}
}
assert.throws(() => container.get(Foo));
});
it('TypeError', function () {
@singleton
class Foo {
public constructor(private readonly test: TypeError) {
}
}
assert.throws(() => container.get(Foo));
});
it('Uint8Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Uint8Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Uint8ClampedArray', function () {
@singleton
class Foo {
public constructor(private readonly test: Uint8ClampedArray) {
}
}
assert.throws(() => container.get(Foo));
});
it('Uint16Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Uint16Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('Uint32Array', function () {
@singleton
class Foo {
public constructor(private readonly test: Uint32Array) {
}
}
assert.throws(() => container.get(Foo));
});
it('UriError', function () {
@singleton
class Foo {
public constructor(private readonly test: URIError) {
}
}
assert.throws(() => container.get(Foo));
});
it('WeakMap', function () {
@singleton
class Foo {
public constructor(private readonly test: WeakMap<any, unknown>) {
}
}
assert.throws(() => container.get(Foo));
});
it('WeakSet', function () {
@singleton
class Foo {
public constructor(private readonly test: WeakSet<any>) {
}
}
assert.throws(() => container.get(Foo));
});
});
describe('good', function () {
it('@all()', function () {
class Foo {
public readonly test: string[] = resolve(all('test')) as string[];
}
assert.deepStrictEqual(container.get(Foo).test, []);
});
it('@optional()', function () {
class Foo {
public readonly test: string = (resolve(optional('test')) ?? null)!;
}
assert.strictEqual(container.get(Foo).test, null);
});
it('undef instance, with constructor default', function () {
container.register(Registration.instance('test', undefined));
class Foo {
public readonly test: string[] = resolve<string[]>('test') ?? [];
}
assert.deepStrictEqual(container.get(Foo).test, []);
});
// TODO: Enable those tests once the decorator metadata is emitted by TS.
// The tests are disabled because TS with TC39 decorators (non-legacy), does not emit the decorator metadata as of now.
// The following tests are dependent on that and hence cannot be successfully run.
// Refer: https://github.com/microsoft/TypeScript/issues/55788
it.skip('can inject if registered', function () {
container.register(Registration.instance(String, 'test'));
@singleton
class Foo {
public constructor(public readonly test: string) {
}
}
assert.deepStrictEqual(container.get(Foo).test, 'test');
});
});
});
describe('@newInstanceOf', function () {
it('throws when there is no registration for interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
assert.throws(() => container.get(newInstanceOf(I)), `No registration for interface: 'I'`);
});
it('instantiates when there is an constructable registration for an interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
class Impl {}
container.register(Registration.singleton(I, Impl));
assert.instanceOf(container.get(newInstanceOf(I)), Impl);
});
it('jit-registers and instantiates when there is a default impl for an interface', function () {
const container = DI.createContainer();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
assert.instanceOf(container.get(newInstanceOf(I)), Impl);
});
it('does not register instance when retrieved through interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
class Impl {}
container.register(Registration.singleton(I, Impl));
assert.notStrictEqual(container.get(newInstanceOf(I)), container.get(I));
assert.strictEqual(container.getAll(I).length, 1);
});
it('does not throw when there is factory registration for an interface', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
let iCallCount = 0;
class IImpl {
public constructor() {
iCallCount++;
}
}
container.registerFactory(I as unknown as Constructable, {
Type: IImpl,
construct(c: IContainer) {
return c.getFactory(this.Type).construct(c);
},
registerTransformer() {
return false;
}
});
const instance1 = container.get(newInstanceOf(I));
assert.strictEqual(iCallCount, 1);
const instance2 = container.get(newInstanceOf(I));
assert.strictEqual(iCallCount, 2);
assert.notStrictEqual(instance1, instance2);
});
it('resolves dependencies from requestor', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
const IDep = DI.createInterface('IDep');
let iCallCount = 0;
class IImpl {
public static get inject() {
return [IDep];
}
public constructor() {
iCallCount++;
}
}
let parentDepCallCount = 0;
let childDepCallCount = 0;
container.registerFactory(I as unknown as Constructable, {
Type: IImpl,
construct(c: IContainer) {
return c.getFactory(this.Type).construct(c);
},
registerTransformer() {
return false;
}
});
container.registerFactory(IDep as unknown as Constructable, {
Type: class {
public constructor() {
parentDepCallCount++;
}
},
construct(c: IContainer) {
return c.getFactory(this.Type).construct(c);
},
registerTransformer() {/* empty */}
});
const childContainer = container.createChild();
class DepImpl {
public constructor() {
childDepCallCount++;
}
}
childContainer.register(Registration.singleton(IDep, DepImpl));
childContainer.registerFactory(IDep as unknown as Constructable, {
Type: DepImpl,
construct(c: IContainer) {
return c.getFactory(this.Type).construct(c);
},
registerTransformer() {/* empty */}
});
const instance = childContainer.get(newInstanceOf(I));
assert.strictEqual(iCallCount, 1);
assert.strictEqual(parentDepCallCount, 0);
assert.strictEqual(childDepCallCount, 1);
assert.instanceOf(instance, IImpl);
});
});
describe('@newInstanceForScope', function () {
it('instantiates, but does not jit-registers, when there is a default impl for an interface', function () {
const container = DI.createContainer();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
assert.instanceOf(container.get(newInstanceForScope(I)), Impl);
assert.strictEqual(container.getAll(I).length, 1);
});
it('does not jit-registers in parent', function () {
const container = DI.createContainer();
const child = container.createChild();
class Impl {}
const I = DI.createInterface('I', x => x.singleton(Impl));
const instance = child.get(newInstanceForScope(I));
assert.instanceOf(instance, Impl);
assert.strictEqual(child.getAll(I).length, 1);
assert.strictEqual(child.getAll(I)[0], instance);
// has resolver, no instance
// has resolver because createNewInstance/scope auto-registers a resolver
// no instance because new instance forScope doesn't register on handler
assert.strictEqual(container.has(I, false), false);
});
// the following test tests a more common, expected scenario,
// where some instance is scoped to a child container,
// instead of the container registering the interface itself.
//
// for the container that registers the interface itself,
// the first registration is always a different resolver,
// with the actual resolver that resolves the instance of the interface
it('creates new instance once in child container', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
let iCallCount = 0;
class IImpl {
public constructor() {
iCallCount++;
}
}
container.register(Registration.singleton(I, IImpl));
container.registerFactory(I as unknown as Constructable, {
Type: IImpl,
construct(c: IContainer) {
return c.getFactory(this.Type).construct(c);
},
registerTransformer() {
return false;
}
});
const childContainer = container.createChild();
const instance = childContainer.get(newInstanceForScope(I));
assert.strictEqual(iCallCount, 1);
assert.instanceOf(instance, IImpl);
const instance2 = childContainer.get(I);
assert.strictEqual(iCallCount, 1);
assert.instanceOf(instance2, IImpl);
assert.strictEqual(instance, instance2);
assert.strictEqual(instance, childContainer.createChild().get(I));
});
});
describe('@factory', function () {
it('resolves different factories', function () {
const container = DI.createContainer();
const I = DI.createInterface('I');
const resolver = factory(I);
const factory1 = container.get(resolver);
const factory2 = container.get(resolver);
assert.notStrictEqual(factory1, factory2);
});
it('invokes new instance when calling the factory', function () {
const container = DI.createContainer();
const f = container.get(factory(class MyClass {
public constructor() {
callCount++;
}
}));
let callCount = 0;
const instance1 = f();
const instance2 = f();
assert.strictEqual(callCount, 2);
assert.notStrictEqual(instance1, instance2);
});
it('resolves with decorator usages', function () {
const container = DI.createContainer();
class MyClass {
public constructor() {
callCount++;
}
}
class MyClassBuilder {
public readonly myClassFactory: IResolvedFactory<MyClass> = resolve(factory(MyClass));
public build() {
return this.myClassFactory();
}
}
let callCount = 0;
const builder = container.get(MyClassBuilder);
const instance1 = builder.build();
const instance2 = builder.build();
assert.strictEqual(callCount, 2);
assert.notStrictEqual(instance1, instance2);
});
it('passes the dynamic parameters', function () {
const container = DI.createContainer();
class MyClass {
public params: unknown[];
public constructor(...params: any[]) {
callCount++;
this.params = params;
}
}
class MyClassBuilder {
public readonly myClassFactory: IResolvedFactory<typeof MyClass> = resolve(factory(MyClass));
public build(...args: unknown[]): MyClass {
return this.myClassFactory(...args);
}
}
let callCount = 0;
const builder = container.get(MyClassBuilder);
const instance1 = builder.build(1, 2, { a: 1, b: 2 });
const instance2 = builder.build(3, 4, { a: 3, b: 4 });
assert.strictEqual(callCount, 2);
assert.notStrictEqual(instance1, instance2);
assert.deepStrictEqual(instance1.params, [1, 2, { a: 1, b: 2 }]);
assert.deepStrictEqual(instance2.params, [3, 4, { a: 3, b: 4 }]);
});
// guess we can add a test for factory resolving to a factory resolving to a factory
// to see if things work smoothly... TODO?
});
describe('resolve', function () {
it('works with resolve(all(...))', function () {
let id = 0;
const II = DI.createInterface<{ a: Model }>();
class Model {
id = ++id;
}
class A {
a = resolve(all(II));
}
container.register(
Registration.transient(II, class I1 {
a = resolve(Model);
}),
Registration.transient(II, class I2 {
a = resolve(newInstanceOf(Model));
})
);
const { a } = container.get(A);
assert.deepStrictEqual(a.map(a => a.a.id), [1, 2]);
});
it('works with a list of keys', function () {
let i = 0;
class Model { v = ++i; }
class Base {
a = resolve(Model, newInstanceOf(Model));
}
const { a: [{ v }, { v: v1 }] } = container.get(Base);
assert.strictEqual(v, 1);
assert.strictEqual(v1, 2);
});
it('works with resolve(transient(...))', function () {
let id = 0;
class Model { id = ++id; }
const { a, b } = container.get(class A {
a = resolve(transient(Model, null!));
b = resolve(transient(Model, null!));
});
assert.deepStrictEqual([a.id, b.id], [1, 2]);
});
it('works with resolve(lazy(...))', function () {
let id = 0;
class Model { id = ++id; }
const { a, b } = container.get(class A {
a = resolve(lazy(Model));
b = resolve(lazy(Model));
});
assert.deepStrictEqual([a().id, b().id], [1, 1]);
assert.deepStrictEqual([a().id, b().id], [1, 1]);
});
it('works with resolve(optional(...))', function () {
let id = 0;
class Model { id = ++id; }
const { a, b } = container.get(class A {
a = resolve(optional(Model));
b = resolve(optional(Model));
});
assert.deepStrictEqual([a?.id, b?.id], [undefined, undefined]);
});
it('works with resolve(newInstanceOf(...))', function () {
let id = 0;
class Model { id = ++id; }
const { a, b } = container.get(class A {
_ = resolve(Model);
a = resolve(newInstanceOf(Model));
b = resolve(newInstanceOf(Model));
});
assert.deepStrictEqual([a.id, b.id], [2, 3]);
assert.strictEqual(container.getAll(Model).length, 1);
});
it('works with resolve(newInstanceForScope(...))', function () {
let id = 0;
class Model { id = ++id; }
const { a, b } = container.get(class A {
_ = resolve(Model);
a = resolve(newInstanceForScope(Model));
b = resolve(newInstanceForScope(Model));
});
assert.deepStrictEqual([a.id, b.id], [2, 3]);
assert.strictEqual(container.getAll(Model).length, 3);
});
it('works with resolve(factory(...))', function () {
let id = 0;
class Model {
id = ++id;
args: number[];
constructor(...args: number[]) {
this.args = args;
}
get sum() {
return this.id + this.args.reduce((c, i) => c + i, 0);
}
}
const { a, b } = container.get(class A {
a = resolve(factory(Model));
b = resolve(factory(Model));
});
assert.deepStrictEqual([a(1, 2).sum, b(1, 2).sum], [4, 5]);
});
it('works with deeply nested resolve(...)', function () {
let i = 0;
class Address { n: number = ++i; }
class Details {
address1 = resolve(Address);
address2 = resolve(newInstanceForScope(Address));
}
class Profile {
details = resolve(Details);
}
class Parent {
model = resolve(Profile);
}
class Child {
parent = resolve(Parent);
}
const { parent } = container.get(Child);
assert.strictEqual(parent.model.details.address1.n, 1);
assert.strictEqual(parent.model.details.address2.n, 2);
assert.strictEqual(parent.model.details, container.get(Details));
});
describe('with [inheritance]', function () {
it('works for basic inheritance', function () {
let i = 0;
class Model { v = ++i; }
class Base {
a = resolve(Model);
}
const { a: { v } } = container.get(class extends Base {});
assert.strictEqual(v, 1);
});
it('works with deeply nested resolve(...)', function () {
let id = 0;
class Model { id = ++id; }
class Value {
a = resolve(newInstanceOf(Model));
}
class V2 extends Value {
a = resolve(newInstanceOf(Model));
}
class V3 extends V2 {
a = resolve(newInstanceOf(Model));
}
const { a } = container.get(V3);
assert.strictEqual(a.id, 3);
});
it('works with a list of keys', function () {
let i = 0;
class Model { v = ++i; }
class Base {
a = resolve(Model, newInstanceOf(Model));
}
const { a: [{ v }, { v: v1 }] } = container.get(class extends Base {});
assert.strictEqual(v, 1);
assert.strictEqual(v1, 2);
});
it('works with multiple all(...)', function () {
let i = 0;
let j = 0;
class Model { v = ++i; }
class Base {
a = resolve(newInstanceForScope(Model), newInstanceForScope(Model));
}
const I = DI.createInterface<Base>();
container.register(
Registration.transient(I, class extends Base {}),
Registration.transient(I, class extends Base {
j = ++j;
}),
);
container.invoke(class {
b = resolve(all(I), all(I));
});
assert.strictEqual(container.getAll(Model).length, 8);
assert.strictEqual(i, 8);
assert.strictEqual(j, 2);
});
});
});
});