UNPKG

@asimojs/asimo

Version:

Asynchronous dependency manager for Typescript projects

454 lines (376 loc) 18.2 kB
import { beforeEach, describe, expect, it } from "vitest"; import { asm as rsm, interfaceId, createContext, AsmContext } from "../asimo"; import { _CalculatorService } from "./calculator"; import { SyncIncrementorIID, _SyncIncrementorService } from "./syncincrementor"; import { AsyncIncrementorIID, _AsyncIncrementorService } from "./asyncincrementor"; import { Multiplier, MultiplierIID, _MultiplierImpl } from "./multiplier"; import { AdderIID, _add } from "./adder"; import { Calculator, CalculatorIID } from "./types"; describe("Asimo", () => { let context: AsmContext; function createTestContext(name?: string) { const c = rsm.createChildContext(name || "test"); // override calculator service c.registerService(CalculatorIID, async () => new _CalculatorService()); c.registerService(SyncIncrementorIID, () => new _SyncIncrementorService()); c.registerService(AsyncIncrementorIID, () => new _AsyncIncrementorService()); c.registerService(AdderIID, () => _add); c.logger = null; return c; } beforeEach(() => { // run tests in an independent context context = createTestContext(); }); it("should be available from globalThis", async () => { const _asm = (globalThis as any)["asm"]; expect(_asm).not.toBe(undefined); expect(_asm).toBe(rsm); }); it("should support name, path and parent properties", async () => { const c1 = createTestContext("test1"); const c2 = createContext({ name: "test2", parent: c1 }); const c3 = createContext("test3"); const c4 = createContext("some/test/with/secial chars"); expect(c1.name).toBe("test1"); expect(c2.name).toBe("test2"); expect(c3.name).toBe("test3"); expect(c4.name).toBe("some\\/test\\/with\\/secial chars"); expect(c1.parent).toBe(rsm); expect(c2.parent).toBe(c1); expect(c3.parent).toBe(null); expect(c4.parent).toBe(null); expect(c1.path).toBe("/asm/test1"); expect(c2.path).toBe("/asm/test1/test2"); expect(c3.path).toBe("/test3"); expect(c4.path).toBe("/some\\/test\\/with\\/secial chars"); }); it("should give a default name to contexts", async () => { const rx = /^AsmContext\d+$/; const c = createContext(); const tc = rsm.createChildContext(); expect(c.name.match(rx)?.length).toBe(1); expect(tc.name.match(rx)?.length).toBe(1); expect(tc.name).not.toBe(c.name); }); it("should list the definitions that have been registered in a given context", async () => { const c = createTestContext("test1"); c.registerFactory(MultiplierIID, () => new _MultiplierImpl()); expect(c.definitions).toMatchObject([ { iid: "asimo.src.tests.Calculator", type: "service", loaded: false }, { iid: "asimo.src.tests.SyncIncrementor", type: "service", loaded: false }, { iid: "asimo.src.tests.AsyncIncrementor", type: "service", loaded: false }, { iid: "asimo.src.tests.Adder", type: "service", loaded: false }, { iid: "asimo.src.tests.Multiplier", type: "object" }, ]); const calc = await c.fetch(CalculatorIID); expect(c.definitions).toMatchObject([ { iid: "asimo.src.tests.Calculator", type: "service", loaded: true }, // now loaded { iid: "asimo.src.tests.SyncIncrementor", type: "service", loaded: false }, { iid: "asimo.src.tests.AsyncIncrementor", type: "service", loaded: false }, { iid: "asimo.src.tests.Adder", type: "service", loaded: false }, { iid: "asimo.src.tests.Multiplier", type: "object" }, ]); expect(calc.add(2, 3)).toBe(5); }); it("should support independent sub-contexts", async function () { const c1 = createTestContext("test1"); const c2 = createTestContext("test2"); expect(c1).not.toBe(c2); const s1 = await c1.fetch(CalculatorIID); const s2 = await c2.fetch(CalculatorIID); expect(s1).not.toBe(s2); expect(s1?.numberOfCalls).toBe(0); expect(s1!.add(1, 2)).toBe(3); expect(s1?.numberOfCalls).toBe(1); expect(s2?.numberOfCalls).toBe(0); expect(s2!.add(1, 2)).toBe(3); expect(s2?.numberOfCalls).toBe(1); const s1bis = await c1.fetch(CalculatorIID)!; expect(s1bis).toBe(s1); expect(s1bis?.numberOfCalls).toBe(1); const s = await context.fetch(CalculatorIID)!; expect(s).not.toBe(s1); expect(s).not.toBe(s2); expect(s?.numberOfCalls).toBe(0); }); it("should support async factories", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, async () => new _CalculatorService()); const calc = await context.fetch(CalcIID)!; expect(calc?.numberOfCalls).toBe(0); expect(calc?.add(21, 21)).toBe(42); expect(calc?.numberOfCalls).toBe(1); }); it("should pass the context to factories", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); let factoryContext: any = null; context.registerService(CalcIID, async (c: AsmContext) => { factoryContext = c; return new _CalculatorService(); }); const calc = await context.fetch(CalcIID)!; expect(calc?.add(21, 21)).toBe(42); expect(factoryContext).toBe(context); }); it("should delegate creation to parent context", async () => { const calc1 = await context.fetch(CalculatorIID)!; calc1?.add(1, 2); const c1 = context.createChildContext(); const calc2 = await c1.fetch(CalculatorIID)!; expect(calc2).toBe(calc1); expect(calc2?.numberOfCalls).toBe(1); }); it("should support get with string namespaces", async () => { const calc1 = await context.fetch(CalculatorIID.ns); (calc1 as Calculator).add(1, 2); const c1 = context.createChildContext(); const calc2 = await c1.fetch(CalculatorIID)!; expect(calc2).toBe(calc1); expect(calc2?.numberOfCalls).toBe(1); }); it("should return null for unknown interfaces", async () => { const lg = context.logger; const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.logger = null; let err = ""; try { const calc = await context.fetch(CalcIID)!; } catch (ex) { err = ex.message; } context.logger = lg; expect(err).toBe('ASM [/asm/test] Interface not found: "asimo.src.tests.Calc"'); }); describe("Invalid factories", () => { it("should return null when factory return undefined", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, () => undefined); const calc = await context.fetch(CalcIID, null)!; expect(calc).toBe(null); }); it("should return null when async factory returns undefined", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, async () => undefined); const calc = await context.fetch(CalcIID, null)!; expect(calc).toBe(null); }); it("should throw when factory does not return an object", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, () => 123 as any); let msg = ""; try { const calc = await context.fetch(CalcIID); } catch (ex) { msg = ex.message; } expect(msg).toBe('ASM [/asm/test] Interface not found: "asimo.src.tests.Calc"'); }); it("should throw when async factory does not return an object", async () => { // create a 2nd interface id for the calculator const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, async () => 123 as any); let msg = ""; try { const calc = await context.fetch(CalcIID)!; } catch (ex) { msg = ex.message; } expect(msg).toBe('ASM [/asm/test] Interface not found: "asimo.src.tests.Calc"'); }); it("should log an error in case factory call error", async () => { let logs: string[] = []; context.logger = { log(msg: any) { logs.push("" + msg); }, }; const CalcIID = interfaceId<Calculator>("asimo.src.tests.Calc"); context.registerService(CalcIID, async () => { throw "Unexpected error"; }); const calc = await context.fetch(CalcIID, null); expect(calc).toBe(null); expect(logs).toEqual([ "ASM [/asm/test] Instantiation error: Unexpected error", 'ASM [/asm/test] Invalid factory output: "asimo.src.tests.Calc"', ]); }); }); describe("Sync style service dependencies", () => { it("should load a service with a custom context", async () => { const calc = (await context.fetch(CalculatorIID))!; const inc = (await context.fetch(SyncIncrementorIID))!; await inc.init(context); expect(calc.numberOfCalls).toBe(0); expect(inc.increment(41)).toBe(42); expect(calc.numberOfCalls).toBe(1); // calc service was called inc.offset = 3; expect(inc.increment(42)).toBe(45); expect(calc.numberOfCalls).toBe(2); // calc service was called }); it("should load a service with the default context", async () => { const rootCalc = (await rsm.fetch(CalculatorIID))!; const calc = (await context.fetch(CalculatorIID))!; const inc = (await context.fetch(SyncIncrementorIID))!; await inc.init(); // no args => rootAsm will be used rootCalc.numberOfCalls = 0; // reset inc.offset = 1; expect(calc.numberOfCalls).toBe(0); expect(inc.increment(41)).toBe(42); expect(rootCalc.numberOfCalls).toBe(1); // calc service was called inc.offset = 3; expect(inc.increment(42)).toBe(45); expect(rootCalc.numberOfCalls).toBe(2); // calc service was called expect(calc.numberOfCalls).toBe(0); // was never called }); it("should support function services", async () => { const add = await context.fetch(AdderIID); expect(add(39, 3)).toBe(42); }); }); describe("Async style service dependencies", () => { it("should load a service with a custom context", async () => { const calc = (await context.fetch(CalculatorIID))!; const inc = (await context.fetch(AsyncIncrementorIID))!; inc.di = context; expect(calc.numberOfCalls).toBe(0); expect(await inc.increment(41)).toBe(42); expect(calc.numberOfCalls).toBe(1); // calc service was called inc.offset = 3; expect(await inc.increment(42)).toBe(45); expect(calc.numberOfCalls).toBe(2); // calc service was called }); it("should load a service with the default context", async () => { const rootCalc = (await rsm.fetch(CalculatorIID))!; const calc = await context.fetch(CalculatorIID); const inc = (await context.fetch(AsyncIncrementorIID))!; rootCalc.numberOfCalls = 0; // reset expect(calc.numberOfCalls).toBe(0); expect(inc.offset).toBe(1); expect(await inc.increment(41)).toBe(42); expect(rootCalc.numberOfCalls).toBe(1); // calc service was called inc.offset = 3; expect(await inc.increment(42)).toBe(45); expect(rootCalc.numberOfCalls).toBe(2); // calc service was called expect(calc.numberOfCalls).toBe(0); // no changes as root calc was used }); }); describe("Object factories", () => { it("should create multiple instances of a given object (root context / get", async () => { const m1 = await context.fetch(MultiplierIID); const m2 = await context.fetch(MultiplierIID); expect(m1).not.toBe(m2); expect(m1.numberOfCalls).toBe(0); expect(m2.numberOfCalls).toBe(0); expect(m1.multiply(2, 4)).toBe(8); expect(m1.multiply(4)).toBe(8); expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(0); expect(m2.multiply(5, 4)).toBe(20); expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(1); }); it("should create multiple instances of a given object (root context) / fetch", async () => { const m1 = (await context.fetch(MultiplierIID))!; const m2 = (await context.fetch(MultiplierIID))!; expect(m1).not.toBe(m2); expect(m1.numberOfCalls).toBe(0); expect(m2.numberOfCalls).toBe(0); expect(m1.multiply(2, 4)).toBe(8); expect(m1.multiply(4)).toBe(8); expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(0); expect(m2.multiply(5, 4)).toBe(20); expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(1); }); it("should return null if object is not found (fetch)", async () => { const calc = await context.fetch("asimo.src.tests.Calc123", null); expect(calc).toBe(null); }); it("should create multiple instances of a given object (sub context) / get", async () => { context.registerFactory(MultiplierIID, () => { const m = new _MultiplierImpl(); m.defaultArg = 3; return m; }); const m1 = await context.fetch(MultiplierIID); const m2 = await context.fetch(MultiplierIID); expect(m1).not.toBe(m2); expect(m1.numberOfCalls).toBe(0); expect(m2.numberOfCalls).toBe(0); expect(m1.multiply(2, 4)).toBe(8); expect(m1.multiply(4)).toBe(12); // defaultArg is 3 expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(0); expect(m2.multiply(5)).toBe(15); // defaultArg is 3 expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(1); }); it("should create multiple instances of a given object (sub context) / fetch", async () => { context.registerFactory(MultiplierIID, () => { const m = new _MultiplierImpl(); m.defaultArg = 3; return m; }); const m1 = (await context.fetch(MultiplierIID))!; const m2 = (await context.fetch(MultiplierIID))!; expect(m1).not.toBe(m2); expect(m1.numberOfCalls).toBe(0); expect(m2.numberOfCalls).toBe(0); expect(m1.multiply(2, 4)).toBe(8); expect(m1.multiply(4)).toBe(12); // defaultArg is 3 expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(0); expect(m2.multiply(5)).toBe(15); // defaultArg is 3 expect(m1.numberOfCalls).toBe(2); expect(m2.numberOfCalls).toBe(1); }); }); describe("Multi getter", () => { it("should return multiple object instances", async () => { const [m1, m2] = await context.fetch(MultiplierIID, MultiplierIID); expect(m1.multiply(2, 5)).toBe(10); expect(m1.numberOfCalls).toBe(1); expect(m2.numberOfCalls).toBe(0); expect(m2.multiply(2, 21)).toBe(42); }); it("should return object and service instances", async () => { const [m, c1, c2] = await context.fetch(MultiplierIID, CalculatorIID, CalculatorIID); expect(m.multiply(2, 5)).toBe(10); expect(c1).toBe(c2); expect(m.numberOfCalls).toBe(1); expect(c1.add(3, 4)).toBe(7); expect(c2.numberOfCalls).toBe(1); // c2 === c1 }); it("should return object and service instances / fetch", async () => { const [m, c1, c2] = await context.fetch(MultiplierIID, CalculatorIID, CalculatorIID); expect(m?.multiply(2, 5)).toBe(10); expect(c1).toBe(c2); expect(m?.numberOfCalls).toBe(1); expect(c1?.add(3, 4)).toBe(7); expect(c2?.numberOfCalls).toBe(1); // c2 === c1 }); it("should return multiple instances through string namespaces", async () => { const [m, c] = await context.fetch(MultiplierIID, CalculatorIID); expect((m as Multiplier).multiply(2, 5)).toBe(10); expect((c as Calculator).add(3, 4)).toBe(7); }); it("should return multiple object instances", async () => { const [m1, m2] = await context.fetch(MultiplierIID, MultiplierIID); expect(m1?.multiply(2, 5)).toBe(10); expect(m1?.numberOfCalls).toBe(1); expect(m2?.numberOfCalls).toBe(0); expect(m2?.multiply(2, 21)).toBe(42); }); }); // TODO // factory crash error // 2 different interface id objects with the same namespace should resolve to the same object });