@asimojs/asimo
Version:
Asynchronous dependency manager for Typescript projects
454 lines (376 loc) • 18.2 kB
text/typescript
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
});