UNPKG

@danielkalen/simplybind

Version:

Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.

925 lines (704 loc) 25 kB
import './setup'; import {Container} from '../src/container'; import {Lazy, All, Optional, Parent, Factory, lazy, all, optional, parent, factory} from '../src/resolvers'; import {transient, singleton} from '../src/registrations'; import {inject} from '../src/injection'; import {decorators} from 'aurelia-metadata'; describe('container', () => { describe('injection', () => { it('instantiates class without injected services', function() { class App {} let container = new Container(); let app = container.get(App); expect(app).toEqual(jasmine.any(App)); }); it('uses static inject method (ES6)', function() { class Logger {} class App { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('uses static inject property (TypeScript,CoffeeScript,ES5)', function() { class Logger {} class App { constructor(logger) { this.logger = logger; } } App.inject = [Logger]; let container = new Container(); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); }); describe('inheritence', function() { class Logger {} class Service {} it('loads dependencies for the parent class', function() { class ParentApp { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class ChildApp extends ParentApp { constructor(...rest) { super(...rest); } } let container = new Container(); let app = container.get(ChildApp); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('loads dependencies for the child class', function() { class ParentApp { } class ChildApp extends ParentApp { static inject() { return [Service]; } constructor(service, ...rest) { super(...rest); this.service = service; } } let container = new Container(); let app = container.get(ChildApp); expect(app.service).toEqual(jasmine.any(Service)); }); it('loads dependencies for both classes', function() { class ParentApp { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class ChildApp extends ParentApp { static inject() { return [Service]; } constructor(service, ...rest) { super(...rest); this.service = service; } } let container = new Container(); let app = container.get(ChildApp); expect(app.service).toEqual(jasmine.any(Service)); expect(app.logger).toEqual(jasmine.any(Logger)); }); }); describe('registration', () => { it('asserts keys are defined', () => { let container = new Container(); expect(() => container.get(null)).toThrow(); expect(() => container.get(undefined)).toThrow(); expect(() => container.registerInstance(null, {})).toThrow(); expect(() => container.registerInstance(undefined, {})).toThrow(); expect(() => container.registerSingleton(null)).toThrow(); expect(() => container.registerSingleton(undefined)).toThrow(); expect(() => container.registerTransient(null)).toThrow(); expect(() => container.registerTransient(undefined)).toThrow(); expect(() => container.autoRegister(null)).toThrow(); expect(() => container.autoRegister(undefined)).toThrow(); expect(() => container.autoRegisterAll([null])).toThrow(); expect(() => container.autoRegisterAll([undefined])).toThrow(); expect(() => container.registerHandler(null)).toThrow(); expect(() => container.registerHandler(undefined)).toThrow(); expect(() => container.hasHandler(null)).toThrow(); expect(() => container.hasHandler(undefined)).toThrow(); }); it('automatically configures as singleton', () => { class Logger {} class App1 { constructor(logger) { this.logger = logger; } } inject(Logger)(App1); class App2 { constructor(logger) { this.logger = logger; } } inject(Logger)(App2); let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).toBe(app2.logger); }); it('automatically configures non-functions as instances', () => { let someObject = {}; class App1 { constructor(something) { this.something = something; } } inject(someObject)(App1); let container = new Container(); let app1 = container.get(App1); expect(app1.something).toBe(someObject); }); it('configures singleton via api', () => { class Logger {} class App1 { constructor(logger) { this.logger = logger; } } inject(Logger)(App1); class App2 { constructor(logger) { this.logger = logger; } } inject(Logger)(App2); let container = new Container(); container.registerSingleton(Logger, Logger); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).toBe(app2.logger); }); it('configures singleton via decorators helper (ES5/6)', () => { let Logger = decorators(singleton()).on(class {}); class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).toBe(app2.logger); }); it('configures transient (non singleton) via api', () => { class Logger {} class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerTransient(Logger, Logger); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).not.toBe(app2.logger); }); it('configures transient (non singleton) via metadata method (ES5/6)', () => { let Logger = decorators(transient()).on(class { }); class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).not.toBe(app2.logger); }); it('configures instance via api', () => { class Logger {} class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let instance = new Logger(); container.registerInstance(Logger, instance); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).toBe(instance); expect(app2.logger).toBe(instance); }); it('configures custom via api', () => { class Logger {} class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerHandler(Logger, c => 'something strange'); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).toEqual('something strange'); expect(app2.logger).toEqual('something strange'); }); it('uses base metadata method (ES5/6) when derived does not specify', () => { let LoggerBase = decorators(transient()).on(class {}); class Logger extends LoggerBase { } class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).not.toBe(app2.logger); }); it('overrides base metadata method (ES5/6) with derived configuration', () => { let LoggerBase = decorators(singleton()).on(class { }); let Logger = decorators(transient()).on(class extends LoggerBase {}); class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).not.toBe(app2.logger); }); it('configures key as service when transient api only provided with key', () => { class Logger {} let container = new Container(); container.registerTransient(Logger); let logger1 = container.get(Logger); let logger2 = container.get(Logger); expect(logger1).toEqual(jasmine.any(Logger)); expect(logger2).toEqual(jasmine.any(Logger)); expect(logger2).not.toBe(logger1); }); it('configures key as service when singleton api only provided with key', () => { class Logger {} let container = new Container(); container.registerSingleton(Logger); let logger1 = container.get(Logger); let logger2 = container.get(Logger); expect(logger1).toEqual(jasmine.any(Logger)); expect(logger2).toEqual(jasmine.any(Logger)); expect(logger2).toBe(logger1); }); it('configures concrete singleton via api for abstract dependency', () => { class LoggerBase {} class Logger extends LoggerBase {} class App { static inject() { return [LoggerBase]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerSingleton(LoggerBase, Logger); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('configures concrete transient via api for abstract dependency', () => { class LoggerBase {} class Logger extends LoggerBase {} class App { static inject() { return [LoggerBase]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerTransient(LoggerBase, Logger); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('doesn\'t get hidden when a super class adds metadata which doesn\'t include the base registration type', () => { let LoggerBase = decorators(transient()).on(class {}); class Logger extends LoggerBase { } Reflect.defineMetadata('something', 'test', Logger); class App1 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } class App2 { static inject() { return [Logger]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app1 = container.get(App1); let app2 = container.get(App2); expect(app1.logger).not.toBe(app2.logger); }); describe('Custom resolvers', () => { describe('Lazy', () => { it('provides a function which, when called, will return the instance', () => { class Logger {} class App1 { static inject() { return [Lazy.of(Logger)]; } constructor(getLogger) { this.getLogger = getLogger; } } let container = new Container(); let app1 = container.get(App1); let logger = app1.getLogger; expect(logger()).toEqual(jasmine.any(Logger)); }); it('provides a function which, when called, will return the instance using decorator', () => { class Logger {} class App1 { static inject = [Logger]; constructor(getLogger) { this.getLogger = getLogger; } } lazy(Logger)(App1, null, 0); let container = new Container(); let app1 = container.get(App1); let logger = app1.getLogger; expect(logger()).toEqual(jasmine.any(Logger)); }); }); describe('All', ()=> { it('resolves all matching dependencies as an array of instances', () => { class LoggerBase {} class VerboseLogger extends LoggerBase {} class Logger extends LoggerBase {} class App { static inject() { return [All.of(LoggerBase)]; } constructor(loggers) { this.loggers = loggers; } } let container = new Container(); container.registerSingleton(LoggerBase, VerboseLogger); container.registerTransient(LoggerBase, Logger); let app = container.get(App); expect(app.loggers).toEqual(jasmine.any(Array)); expect(app.loggers.length).toBe(2); expect(app.loggers[0]).toEqual(jasmine.any(VerboseLogger)); expect(app.loggers[1]).toEqual(jasmine.any(Logger)); }); it('resolves all matching dependencies as an array of instances using decorator', () => { class LoggerBase {} class VerboseLogger extends LoggerBase {} class Logger extends LoggerBase {} class App { static inject = [LoggerBase]; constructor(loggers) { this.loggers = loggers; } } all(LoggerBase)(App, null, 0); let container = new Container(); container.registerSingleton(LoggerBase, VerboseLogger); container.registerTransient(LoggerBase, Logger); let app = container.get(App); expect(app.loggers).toEqual(jasmine.any(Array)); expect(app.loggers.length).toBe(2); expect(app.loggers[0]).toEqual(jasmine.any(VerboseLogger)); expect(app.loggers[1]).toEqual(jasmine.any(Logger)); }); }); describe('inject as param decorator', ()=> { it('resolves a matching dependency using the inject decorator', () => { class Logger {} class App1 { static inject = [Logger]; constructor(logger) { this.logger = logger; } } inject(Logger)(App1, null, 0); let container = new Container(); let app1 = container.get(App1); let logger = app1.logger; expect(logger).toEqual(jasmine.any(Logger)); }); }); describe('Optional', ()=> { it('injects the instance if its registered in the container', () => { class Logger {} class App { static inject() { return [Optional.of(Logger)]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerSingleton(Logger, Logger); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('injects the instance if its registered in the container using decorator', () => { class Logger {} class App { static inject = [Logger]; constructor(logger) { this.logger = logger; } } optional(Logger)(App, null, 0); let container = new Container(); container.registerSingleton(Logger, Logger); let app = container.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); it('injects null if key is not registered in the container', () => { class VerboseLogger {} class Logger {} class App { static inject() { return [Optional.of(Logger)]; } constructor(logger) { this.logger = logger; } } let container = new Container(); container.registerSingleton(VerboseLogger, Logger); let app = container.get(App); expect(app.logger).toBe(null); }); it('injects null if key is not registered in the container using decorator', () => { class VerboseLogger {} class Logger {} class App { static inject = [Logger]; constructor(logger) { this.logger = logger; } } optional(Logger)(App, null, 0); let container = new Container(); container.registerSingleton(VerboseLogger, Logger); let app = container.get(App); expect(app.logger).toBe(null); }); it('injects null if key nor function is registered in the container', () => { class VerboseLogger {} class Logger {} class App { static inject() { return [Optional.of(Logger)]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let app = container.get(App); expect(app.logger).toBe(null); }); it('doesn\'t check the parent container hierarchy when checkParent is false', () => { class Logger {} class App { static inject() { return [Optional.of(Logger, false)]; } constructor(logger) { this.logger = logger; } } let parentContainer = new Container(); parentContainer.registerSingleton(Logger, Logger); let childContainer = parentContainer.createChild(); childContainer.registerSingleton(App, App); let app = childContainer.get(App); expect(app.logger).toBe(null); }); it('checks the parent container hierarchy when checkParent is true or default', () => { class Logger {} class App { static inject() { return [Optional.of(Logger)]; } constructor(logger) { this.logger = logger; } } let parentContainer = new Container(); parentContainer.registerSingleton(Logger, Logger); let childContainer = parentContainer.createChild(); childContainer.registerSingleton(App, App); let app = childContainer.get(App); expect(app.logger).toEqual(jasmine.any(Logger)); }); }); describe('Parent', ()=> { it('bypasses the current container and injects instance from parent container', () =>{ class Logger {} class App { static inject() { return [Parent.of(Logger)]; } constructor(logger) { this.logger = logger; } } let parentContainer = new Container(); let parentInstance = new Logger(); parentContainer.registerInstance(Logger, parentInstance); let childContainer = parentContainer.createChild(); let childInstance = new Logger(); childContainer.registerInstance(Logger, childInstance); childContainer.registerSingleton(App, App); let app = childContainer.get(App); expect(app.logger).toBe(parentInstance); }); it('bypasses the current container and injects instance from parent container using decorator', () =>{ class Logger {} class App { static inject = [Logger]; constructor(logger) { this.logger = logger; } } parent(App, null, 0); let parentContainer = new Container(); let parentInstance = new Logger(); parentContainer.registerInstance(Logger, parentInstance); let childContainer = parentContainer.createChild(); let childInstance = new Logger(); childContainer.registerInstance(Logger, childInstance); childContainer.registerSingleton(App, App); let app = childContainer.get(App); expect(app.logger).toBe(parentInstance); }); it('returns null when no parent container exists', () =>{ class Logger {} class App { static inject() { return [Parent.of(Logger)]; } constructor(logger) { this.logger = logger; } } let container = new Container(); let instance = new Logger(); container.registerInstance(Logger, instance); let app = container.get(App); expect(app.logger).toBe(null); }); it('returns null when no parent container exists using decorator', () =>{ class Logger {} class App { static inject = [Logger]; constructor(logger) { this.logger = logger; } } parent(App, null, 0); let container = new Container(); let instance = new Logger(); container.registerInstance(Logger, instance); let app = container.get(App); expect(app.logger).toBe(null); }); }); describe('Factory', () => { let container; let app; let logger; let service; let data = 'test'; class Logger {} class Service { static inject() { return [Factory.of(Logger)]; } constructor(getLogger, data) { this.getLogger = getLogger; this.data = data; } } class App { static inject() { return [Factory.of(Service)]; } constructor(GetService) { this.GetService = GetService; this.service = new GetService(data); } } beforeEach(() => { container = new Container(); }); it('provides a function which, when called, will return the instance', () => { app = container.get(App); service = app.GetService; expect(service()).toEqual(jasmine.any(Service)); }); it('passes data in to the constructor as the second argument', () => { app = container.get(App); expect(app.service.data).toEqual(data); }); }); describe('Factory decorator', () => { let container; let app; let logger; let service; let data = 'test'; class Logger {} class Service { static inject= [Logger]; constructor(getLogger, data) { this.getLogger = getLogger; this.data = data; } } factory(Logger)(Service, null, 0); class App { static inject = [Service]; constructor(GetService) { this.GetService = GetService; this.service = new GetService(data); } } factory(Service)(App, null, 0); beforeEach(() => { container = new Container(); }); it('provides a function which, when called, will return the instance', () => { app = container.get(App); service = app.GetService; expect(service()).toEqual(jasmine.any(Service)); }); it('passes data in to the constructor as the second argument', () => { app = container.get(App); expect(app.service.data).toEqual(data); }); }); }); }); });