@routecraft/testing 0.3.0-canary.2 → 0.3.0-canary.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -4
- package/dist/index.d.ts +58 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,10 +48,14 @@ describe("my route", () => {
|
|
|
48
48
|
- **`ctx`** — The underlying context.
|
|
49
49
|
- **`logger`** — A spy logger (Vitest `vi.fn()` methods) for asserting on log calls.
|
|
50
50
|
- **`errors`** — Collected route errors.
|
|
51
|
-
- **`test()`** — Runs start → wait for routes ready → drain → stop. Assert after `await t.test()`.
|
|
52
|
-
- **`
|
|
53
|
-
- **`
|
|
51
|
+
- **`test(options?)`** — Runs start → wait for routes ready → (optional delay) → drain → stop. Assert after `await t.test()`. Options:
|
|
52
|
+
- **`delayBeforeDrainMs`** — Wait this many ms after routes are ready before draining. Use for **timer** (or other deferred) sources so at least one message is processed; e.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with `intervalMs: 50`.
|
|
53
|
+
- **`startAndWaitReady()`** — Start context and wait for all routes to be ready (no drain/stop). Use with **`invoke()`** to call a route by id, then call **`stop()`** (or **`drain()`** then **`stop()`**) when done.
|
|
54
|
+
- **`stop()`** / **`drain()`** — Lifecycle helpers.
|
|
55
|
+
- **`TestContextOptions`** — Builder options (e.g. `routesReadyTimeoutMs`).
|
|
56
|
+
- **`TestOptions`** — Options for `test()` (e.g. `delayBeforeDrainMs`).
|
|
54
57
|
- **`SpyLogger`** — Type for the spy logger on `t.logger`.
|
|
58
|
+
- **`invoke(ctx, routeIdOrDestination, body, headers?)`** — Invoke a route by id (string) or send to a Destination instance; returns the result. Use route id when the route's source implements Destination (e.g. direct adapter): `await invoke(t.ctx, "my-route-id", { ... })`.
|
|
55
59
|
|
|
56
60
|
## Documentation
|
|
57
61
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var vitest=require('vitest'),routecraft=require('@routecraft/routecraft');var
|
|
1
|
+
'use strict';var fs=require('fs'),vitest=require('vitest'),routecraft=require('@routecraft/routecraft');var L=200;function S(){let r={info:vitest.vi.fn(),debug:vitest.vi.fn(),warn:vitest.vi.fn(),error:vitest.vi.fn(),trace:vitest.vi.fn(),fatal:vitest.vi.fn(),child:vitest.vi.fn()};return r.child.mockImplementation(()=>r),r}function P(){let r=vitest.vi.fn(),e=vitest.vi.fn(),t={info:r,debug:r,warn:r,error:r,trace:r,fatal:r,child:e};return e.mockImplementation(()=>t),t}var p=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;startedPromise;constructor(e,t){this.ctx=e,this.logger=t?.spyLogger??P(),t?.restoreLoggerChild&&(this.restoreLoggerChild=t.restoreLoggerChild),this.routesReadyTimeoutMs=t?.routesReadyTimeoutMs??L,e.on("error",o=>{let i=o.details.error;this.errors.push(routecraft.isRouteCraftError(i)?i:routecraft.rcError("RC9901",i));});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((i,u)=>{let f=0,n=false,d=setTimeout(()=>{n||(n=true,a(),c(),u(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),a=e.on("routeStarted",()=>{n||(f++,f>=t&&(n=true,clearTimeout(d),a(),c(),i()));}),c=e.on("error",y=>{n||(n=true,clearTimeout(d),a(),c(),u(y.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let t=this.ctx,o=t.getRoutes().length,i=o===0?Promise.resolve():new Promise((f,n)=>{let d=0,a=false,c=setTimeout(()=>{a||(g(),n(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),y=t.on("routeStarted",()=>{a||(d++,d>=o&&(g(),f()));}),h=t.on("error",R=>{a||(g(),n(R.details.error));});function g(){a||(a=true,y(),h(),c!==void 0&&(clearTimeout(c),c=void 0));}}),u=t.start();try{await i;let f=e?.delayBeforeDrainMs??0;f>0&&await new Promise(n=>setTimeout(n,f)),await t.drain();}finally{try{await t.stop(),await u;}finally{this.restoreLoggerChild?.();}}}drain(){return this.ctx.drain()}async stop(){await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}},m=class{builder=new routecraft.ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,t){return this.builder.on(e,t),this}store(e,t){return this.builder.store(e,t),this}routes(e){return this.builder.routes(e),this}async build(){let e=S(),t=routecraft.logger.child.bind(routecraft.logger);routecraft.logger.child=vitest.vi.fn(()=>e);let o=await this.builder.build(),i={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{routecraft.logger.child=t;}};return new p(o,i)}};function K(){return new m}function b(r){return typeof r=="object"&&r!==null&&typeof r.send=="function"}async function O(r,e,t,o){let i=new routecraft.DefaultExchange(r,{body:t,...o!==void 0&&{headers:o}}),u;if(typeof e=="string"){let n=r.getRouteById(e);if(!n)throw new Error(`No route with id "${e}". Did you start the context (e.g. await t.test())?`);let d=n.definition.source;if(!b(d))throw new Error(`Route "${e}" is not invokable: source must implement Destination (e.g. direct adapter).`);u=d;}else u=e;return await u.send(i)}function k(r){return JSON.parse(fs.readFileSync(r,"utf-8"))}function A(r,e){let t=k(r);if(!Array.isArray(t))throw new Error(`fixture.each: expected JSON array at "${r}", got ${typeof t}`);for(let o of t){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);vitest.test(o.name,()=>e(o));}}exports.TestContext=p;exports.TestContextBuilder=m;exports.fixture=k;exports.fixtureEach=A;exports.invoke=O;exports.testContext=K;//# sourceMappingURL=index.cjs.map
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_ROUTES_READY_TIMEOUT_MS","createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","cleanup","offRouteStarted","offError","started","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n isRouteCraftError,\n RouteCraftError,\n error as rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { EventName, EventHandler } from \"@routecraft/routecraft\";\nimport type { RouteDefinition, RouteBuilder } from \"@routecraft/routecraft\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nfunction createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context, wait for all routes ready, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n */\n async test(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n stop(): Promise<void> {\n return this.ctx.stop();\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_ROUTES_READY_TIMEOUT_MS","createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","isDestination","obj","invoke","routeIdOrDestination","body","headers","exchange","DefaultExchange","dest","route","source","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.cjs","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { vi, test } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n DefaultExchange,\n isRouteCraftError,\n RouteCraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type {\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n Destination,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nfunction createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n\n/** Duck-type: object with send(exchange) returning Promise. */\nfunction isDestination(obj: unknown): obj is Destination<unknown, unknown> {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n typeof (obj as { send?: unknown }).send === \"function\"\n );\n}\n\n/**\n * Invoke a route by id or send to a destination and return the result.\n *\n * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).\n * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple\n * routes in the default export — use the route's id.\n *\n * - **By destination:** Pass a Destination instance (e.g. direct(\"endpoint\")). Builds an\n * exchange and calls destination.send(exchange).\n *\n * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started\n * @param routeIdOrDestination Route id string or a Destination adapter instance\n * @param body Request body\n * @param headers Optional headers for the exchange\n * @returns The result from the route or destination (e.g. response body for DirectAdapter)\n */\nexport async function invoke<T = unknown, R = T>(\n ctx: CraftContext,\n routeIdOrDestination: string | Destination<T, R>,\n body: T,\n headers?: ExchangeHeaders,\n): Promise<R> {\n const exchange = new DefaultExchange(ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n let dest: Destination<T, R>;\n\n if (typeof routeIdOrDestination === \"string\") {\n const route = ctx.getRouteById(routeIdOrDestination);\n if (!route) {\n throw new Error(\n `No route with id \"${routeIdOrDestination}\". Did you start the context (e.g. await t.test())?`,\n );\n }\n const source = route.definition.source;\n if (!isDestination(source)) {\n throw new Error(\n `Route \"${routeIdOrDestination}\" is not invokable: source must implement Destination (e.g. direct adapter).`,\n );\n }\n dest = source as Destination<T, R>;\n } else {\n dest = routeIdOrDestination;\n }\n\n const result = await dest.send(exchange as Parameters<typeof dest.send>[0]);\n return result as R;\n}\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { vi } from 'vitest';
|
|
2
|
-
import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder } from '@routecraft/routecraft';
|
|
2
|
+
import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, ExchangeHeaders } from '@routecraft/routecraft';
|
|
3
3
|
|
|
4
4
|
interface TestContextOptions {
|
|
5
5
|
/** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
|
|
6
6
|
routesReadyTimeoutMs?: number;
|
|
7
7
|
}
|
|
8
|
+
/** Options for TestContext.test(). */
|
|
9
|
+
interface TestOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Delay in ms after all routes are ready, before draining.
|
|
12
|
+
* Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
|
|
13
|
+
* E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.
|
|
14
|
+
*/
|
|
15
|
+
delayBeforeDrainMs?: number;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
|
|
10
19
|
*/
|
|
@@ -29,15 +38,24 @@ declare class TestContext {
|
|
|
29
38
|
readonly errors: RouteCraftError[];
|
|
30
39
|
private readonly routesReadyTimeoutMs;
|
|
31
40
|
private restoreLoggerChild?;
|
|
41
|
+
private startedPromise?;
|
|
32
42
|
constructor(ctx: CraftContext, options?: TestContextOptions & {
|
|
33
43
|
spyLogger?: SpyLogger;
|
|
34
44
|
restoreLoggerChild?: () => void;
|
|
35
45
|
});
|
|
36
46
|
/**
|
|
37
|
-
* Start context
|
|
47
|
+
* Start context and wait for all routes to be ready. Does not drain or stop.
|
|
48
|
+
* Use with invoke() to send to a route by id, then call drain()/stop() when done.
|
|
49
|
+
*/
|
|
50
|
+
startAndWaitReady(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.
|
|
38
53
|
* Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).
|
|
54
|
+
*
|
|
55
|
+
* @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.
|
|
56
|
+
* Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
|
|
39
57
|
*/
|
|
40
|
-
test(): Promise<void>;
|
|
58
|
+
test(options?: TestOptions): Promise<void>;
|
|
41
59
|
drain(): Promise<void>;
|
|
42
60
|
stop(): Promise<void>;
|
|
43
61
|
}
|
|
@@ -65,5 +83,41 @@ declare class TestContextBuilder {
|
|
|
65
83
|
* await t.test();
|
|
66
84
|
*/
|
|
67
85
|
declare function testContext(): TestContextBuilder;
|
|
86
|
+
/**
|
|
87
|
+
* Invoke a route by id or send to a destination and return the result.
|
|
88
|
+
*
|
|
89
|
+
* - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).
|
|
90
|
+
* The route's source must implement Destination (e.g. DirectAdapter). Works with multiple
|
|
91
|
+
* routes in the default export — use the route's id.
|
|
92
|
+
*
|
|
93
|
+
* - **By destination:** Pass a Destination instance (e.g. direct("endpoint")). Builds an
|
|
94
|
+
* exchange and calls destination.send(exchange).
|
|
95
|
+
*
|
|
96
|
+
* @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started
|
|
97
|
+
* @param routeIdOrDestination Route id string or a Destination adapter instance
|
|
98
|
+
* @param body Request body
|
|
99
|
+
* @param headers Optional headers for the exchange
|
|
100
|
+
* @returns The result from the route or destination (e.g. response body for DirectAdapter)
|
|
101
|
+
*/
|
|
102
|
+
declare function invoke<T = unknown, R = T>(ctx: CraftContext, routeIdOrDestination: string | Destination<T, R>, body: T, headers?: ExchangeHeaders): Promise<R>;
|
|
103
|
+
/**
|
|
104
|
+
* Load a JSON fixture file and return the parsed value.
|
|
105
|
+
*
|
|
106
|
+
* @param path Absolute or relative path to the JSON file
|
|
107
|
+
* @returns Parsed JSON as T
|
|
108
|
+
*/
|
|
109
|
+
declare function fixture<T = unknown>(path: string): T;
|
|
110
|
+
/** Fixture entry must have a `name` field used as the vitest test name. */
|
|
111
|
+
interface FixtureWithName {
|
|
112
|
+
name: string;
|
|
113
|
+
[key: string]: unknown;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).
|
|
117
|
+
*
|
|
118
|
+
* @param path Path to a JSON file that parses to an array
|
|
119
|
+
* @param run Callback invoked per entry; use for assertions. Receives the fixture entry.
|
|
120
|
+
*/
|
|
121
|
+
declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
|
|
68
122
|
|
|
69
|
-
export { type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, testContext };
|
|
123
|
+
export { type FixtureWithName, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, fixture, fixtureEach, invoke, testContext };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { vi } from 'vitest';
|
|
2
|
-
import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder } from '@routecraft/routecraft';
|
|
2
|
+
import { CraftContext, RouteCraftError, CraftConfig, EventName, EventHandler, StoreRegistry, RouteDefinition, RouteBuilder, Destination, ExchangeHeaders } from '@routecraft/routecraft';
|
|
3
3
|
|
|
4
4
|
interface TestContextOptions {
|
|
5
5
|
/** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */
|
|
6
6
|
routesReadyTimeoutMs?: number;
|
|
7
7
|
}
|
|
8
|
+
/** Options for TestContext.test(). */
|
|
9
|
+
interface TestOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Delay in ms after all routes are ready, before draining.
|
|
12
|
+
* Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
|
|
13
|
+
* E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.
|
|
14
|
+
*/
|
|
15
|
+
delayBeforeDrainMs?: number;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).
|
|
10
19
|
*/
|
|
@@ -29,15 +38,24 @@ declare class TestContext {
|
|
|
29
38
|
readonly errors: RouteCraftError[];
|
|
30
39
|
private readonly routesReadyTimeoutMs;
|
|
31
40
|
private restoreLoggerChild?;
|
|
41
|
+
private startedPromise?;
|
|
32
42
|
constructor(ctx: CraftContext, options?: TestContextOptions & {
|
|
33
43
|
spyLogger?: SpyLogger;
|
|
34
44
|
restoreLoggerChild?: () => void;
|
|
35
45
|
});
|
|
36
46
|
/**
|
|
37
|
-
* Start context
|
|
47
|
+
* Start context and wait for all routes to be ready. Does not drain or stop.
|
|
48
|
+
* Use with invoke() to send to a route by id, then call drain()/stop() when done.
|
|
49
|
+
*/
|
|
50
|
+
startAndWaitReady(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.
|
|
38
53
|
* Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).
|
|
54
|
+
*
|
|
55
|
+
* @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.
|
|
56
|
+
* Use for timer (or other deferred) sources so at least one message is processed before drain/stop.
|
|
39
57
|
*/
|
|
40
|
-
test(): Promise<void>;
|
|
58
|
+
test(options?: TestOptions): Promise<void>;
|
|
41
59
|
drain(): Promise<void>;
|
|
42
60
|
stop(): Promise<void>;
|
|
43
61
|
}
|
|
@@ -65,5 +83,41 @@ declare class TestContextBuilder {
|
|
|
65
83
|
* await t.test();
|
|
66
84
|
*/
|
|
67
85
|
declare function testContext(): TestContextBuilder;
|
|
86
|
+
/**
|
|
87
|
+
* Invoke a route by id or send to a destination and return the result.
|
|
88
|
+
*
|
|
89
|
+
* - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).
|
|
90
|
+
* The route's source must implement Destination (e.g. DirectAdapter). Works with multiple
|
|
91
|
+
* routes in the default export — use the route's id.
|
|
92
|
+
*
|
|
93
|
+
* - **By destination:** Pass a Destination instance (e.g. direct("endpoint")). Builds an
|
|
94
|
+
* exchange and calls destination.send(exchange).
|
|
95
|
+
*
|
|
96
|
+
* @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started
|
|
97
|
+
* @param routeIdOrDestination Route id string or a Destination adapter instance
|
|
98
|
+
* @param body Request body
|
|
99
|
+
* @param headers Optional headers for the exchange
|
|
100
|
+
* @returns The result from the route or destination (e.g. response body for DirectAdapter)
|
|
101
|
+
*/
|
|
102
|
+
declare function invoke<T = unknown, R = T>(ctx: CraftContext, routeIdOrDestination: string | Destination<T, R>, body: T, headers?: ExchangeHeaders): Promise<R>;
|
|
103
|
+
/**
|
|
104
|
+
* Load a JSON fixture file and return the parsed value.
|
|
105
|
+
*
|
|
106
|
+
* @param path Absolute or relative path to the JSON file
|
|
107
|
+
* @returns Parsed JSON as T
|
|
108
|
+
*/
|
|
109
|
+
declare function fixture<T = unknown>(path: string): T;
|
|
110
|
+
/** Fixture entry must have a `name` field used as the vitest test name. */
|
|
111
|
+
interface FixtureWithName {
|
|
112
|
+
name: string;
|
|
113
|
+
[key: string]: unknown;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).
|
|
117
|
+
*
|
|
118
|
+
* @param path Path to a JSON file that parses to an array
|
|
119
|
+
* @param run Callback invoked per entry; use for assertions. Receives the fixture entry.
|
|
120
|
+
*/
|
|
121
|
+
declare function fixtureEach<T extends FixtureWithName>(path: string, run: (entry: T) => void | Promise<void>): void;
|
|
68
122
|
|
|
69
|
-
export { type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, testContext };
|
|
123
|
+
export { type FixtureWithName, type SpyLogger, TestContext, TestContextBuilder, type TestContextOptions, type TestOptions, fixture, fixtureEach, invoke, testContext };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {vi}from'vitest';import {isRouteCraftError,
|
|
1
|
+
import {readFileSync}from'fs';import {vi,test}from'vitest';import {isRouteCraftError,rcError,ContextBuilder,logger,DefaultExchange}from'@routecraft/routecraft';var L=200;function S(){let r={info:vi.fn(),debug:vi.fn(),warn:vi.fn(),error:vi.fn(),trace:vi.fn(),fatal:vi.fn(),child:vi.fn()};return r.child.mockImplementation(()=>r),r}function P(){let r=vi.fn(),e=vi.fn(),t={info:r,debug:r,warn:r,error:r,trace:r,fatal:r,child:e};return e.mockImplementation(()=>t),t}var p=class{ctx;logger;errors=[];routesReadyTimeoutMs;restoreLoggerChild;startedPromise;constructor(e,t){this.ctx=e,this.logger=t?.spyLogger??P(),t?.restoreLoggerChild&&(this.restoreLoggerChild=t.restoreLoggerChild),this.routesReadyTimeoutMs=t?.routesReadyTimeoutMs??L,e.on("error",o=>{let i=o.details.error;this.errors.push(isRouteCraftError(i)?i:rcError("RC9901",i));});}async startAndWaitReady(){let e=this.ctx,t=e.getRoutes().length,o=t===0?Promise.resolve():new Promise((i,u)=>{let f=0,n=false,d=setTimeout(()=>{n||(n=true,a(),c(),u(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),a=e.on("routeStarted",()=>{n||(f++,f>=t&&(n=true,clearTimeout(d),a(),c(),i()));}),c=e.on("error",y=>{n||(n=true,clearTimeout(d),a(),c(),u(y.details.error));});});this.startedPromise=e.start(),await Promise.all([this.startedPromise,o]);}async test(e){let t=this.ctx,o=t.getRoutes().length,i=o===0?Promise.resolve():new Promise((f,n)=>{let d=0,a=false,c=setTimeout(()=>{a||(g(),n(new Error("Timeout waiting for routes to start")));},this.routesReadyTimeoutMs),y=t.on("routeStarted",()=>{a||(d++,d>=o&&(g(),f()));}),h=t.on("error",R=>{a||(g(),n(R.details.error));});function g(){a||(a=true,y(),h(),c!==void 0&&(clearTimeout(c),c=void 0));}}),u=t.start();try{await i;let f=e?.delayBeforeDrainMs??0;f>0&&await new Promise(n=>setTimeout(n,f)),await t.drain();}finally{try{await t.stop(),await u;}finally{this.restoreLoggerChild?.();}}}drain(){return this.ctx.drain()}async stop(){await this.ctx.stop(),this.startedPromise!==void 0&&await this.startedPromise;}},m=class{builder=new ContextBuilder;routesReadyTimeoutMs;routesReadyTimeout(e){return this.routesReadyTimeoutMs=e,this}with(e){return this.builder.with(e),this}on(e,t){return this.builder.on(e,t),this}store(e,t){return this.builder.store(e,t),this}routes(e){return this.builder.routes(e),this}async build(){let e=S(),t=logger.child.bind(logger);logger.child=vi.fn(()=>e);let o=await this.builder.build(),i={...this.routesReadyTimeoutMs!==void 0?{routesReadyTimeoutMs:this.routesReadyTimeoutMs}:{},spyLogger:e,restoreLoggerChild:()=>{logger.child=t;}};return new p(o,i)}};function K(){return new m}function b(r){return typeof r=="object"&&r!==null&&typeof r.send=="function"}async function O(r,e,t,o){let i=new DefaultExchange(r,{body:t,...o!==void 0&&{headers:o}}),u;if(typeof e=="string"){let n=r.getRouteById(e);if(!n)throw new Error(`No route with id "${e}". Did you start the context (e.g. await t.test())?`);let d=n.definition.source;if(!b(d))throw new Error(`Route "${e}" is not invokable: source must implement Destination (e.g. direct adapter).`);u=d;}else u=e;return await u.send(i)}function k(r){return JSON.parse(readFileSync(r,"utf-8"))}function A(r,e){let t=k(r);if(!Array.isArray(t))throw new Error(`fixture.each: expected JSON array at "${r}", got ${typeof t}`);for(let o of t){if(typeof o?.name!="string")throw new Error(`fixture.each: each entry must have a "name" field (string). Got: ${JSON.stringify(o)}`);test(o.name,()=>e(o));}}export{p as TestContext,m as TestContextBuilder,k as fixture,A as fixtureEach,O as invoke,K as testContext};//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_ROUTES_READY_TIMEOUT_MS","createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","cleanup","offRouteStarted","offError","started","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext"],"mappings":"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","file":"index.js","sourcesContent":["import { vi } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n isRouteCraftError,\n RouteCraftError,\n error as rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type { EventName, EventHandler } from \"@routecraft/routecraft\";\nimport type { RouteDefinition, RouteBuilder } from \"@routecraft/routecraft\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nfunction createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context, wait for all routes ready, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n */\n async test(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n stop(): Promise<void> {\n return this.ctx.stop();\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_ROUTES_READY_TIMEOUT_MS","createSpyLogger","spy","vi","createNoopSpyLogger","noop","childFn","noopLogger","TestContext","ctx","options","payload","err","isRouteCraftError","rcError","total","allReady","resolve","reject","ready","settled","timeoutId","offRouteStarted","offError","cleanup","started","delayMs","TestContextBuilder","ContextBuilder","ms","config","event","handler","key","value","routes","spyLogger","originalChild","logger","testContext","isDestination","obj","invoke","routeIdOrDestination","body","headers","exchange","DefaultExchange","dest","route","source","fixture","path","readFileSync","fixtureEach","run","entries","entry","test"],"mappings":"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","file":"index.js","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { vi, test } from \"vitest\";\nimport type {\n CraftContext,\n CraftConfig,\n StoreRegistry,\n} from \"@routecraft/routecraft\";\nimport {\n ContextBuilder,\n DefaultExchange,\n isRouteCraftError,\n RouteCraftError,\n rcError,\n logger,\n} from \"@routecraft/routecraft\";\nimport type {\n EventName,\n EventHandler,\n RouteDefinition,\n RouteBuilder,\n Destination,\n ExchangeHeaders,\n} from \"@routecraft/routecraft\";\n\nconst DEFAULT_ROUTES_READY_TIMEOUT_MS = 200;\n\nfunction createSpyLogger(): SpyLogger {\n const spy: SpyLogger = {\n info: vi.fn(),\n debug: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n trace: vi.fn(),\n fatal: vi.fn(),\n child: vi.fn(),\n };\n spy.child.mockImplementation(() => spy);\n return spy;\n}\n\nfunction createNoopSpyLogger(): SpyLogger {\n const noop = vi.fn();\n const childFn = vi.fn();\n const noopLogger: SpyLogger = {\n info: noop,\n debug: noop,\n warn: noop,\n error: noop,\n trace: noop,\n fatal: noop,\n child: childFn,\n };\n childFn.mockImplementation(() => noopLogger);\n return noopLogger;\n}\n\nexport interface TestContextOptions {\n /** Timeout in ms for waiting for all routes to emit routeStarted. Default 200. */\n routesReadyTimeoutMs?: number;\n}\n\n/** Options for TestContext.test(). */\nexport interface TestOptions {\n /**\n * Delay in ms after all routes are ready, before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n * E.g. `await t.test({ delayBeforeDrainMs: 50 })` for a timer with intervalMs >= 50.\n */\n delayBeforeDrainMs?: number;\n}\n\n/**\n * Spy logger with vi.fn() methods for assertions (e.g. expect(t.logger.info).toHaveBeenCalledWith(...)).\n */\nexport type SpyLogger = {\n info: ReturnType<typeof vi.fn>;\n debug: ReturnType<typeof vi.fn>;\n warn: ReturnType<typeof vi.fn>;\n error: ReturnType<typeof vi.fn>;\n trace: ReturnType<typeof vi.fn>;\n fatal: ReturnType<typeof vi.fn>;\n child: ReturnType<typeof vi.fn>;\n};\n\n/**\n * Test-friendly wrapper around CraftContext. Runs the real context but manages\n * lifecycle (start → wait routes ready → drain → stop) and collects errors.\n * t.logger is a spy logger (vi.fn() methods) for asserting on log calls.\n */\nexport class TestContext {\n readonly ctx: CraftContext;\n /** Spy logger; e.g. expect(t.logger.info).toHaveBeenCalledWith(...) */\n readonly logger: SpyLogger;\n readonly errors: RouteCraftError[] = [];\n private readonly routesReadyTimeoutMs: number;\n\n private restoreLoggerChild?: () => void;\n private startedPromise?: Promise<void>;\n\n constructor(\n ctx: CraftContext,\n options?: TestContextOptions & {\n spyLogger?: SpyLogger;\n restoreLoggerChild?: () => void;\n },\n ) {\n this.ctx = ctx;\n this.logger = options?.spyLogger ?? createNoopSpyLogger();\n if (options?.restoreLoggerChild)\n this.restoreLoggerChild = options.restoreLoggerChild;\n this.routesReadyTimeoutMs =\n options?.routesReadyTimeoutMs ?? DEFAULT_ROUTES_READY_TIMEOUT_MS;\n ctx.on(\"error\", (payload) => {\n const err = payload.details.error;\n this.errors.push(\n isRouteCraftError(err)\n ? (err as RouteCraftError)\n : rcError(\"RC9901\", err),\n );\n });\n }\n\n /**\n * Start context and wait for all routes to be ready. Does not drain or stop.\n * Use with invoke() to send to a route by id, then call drain()/stop() when done.\n */\n async startAndWaitReady(): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n const timeoutId = setTimeout(() => {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n offRouteStarted();\n offError();\n reject(payload.details.error);\n });\n });\n this.startedPromise = ctx.start();\n await Promise.all([this.startedPromise, allReady]);\n }\n\n /**\n * Start context, wait for all routes ready, optionally delay, drain in-flight, then stop.\n * Assert after this returns (mocks, t.errors, t.ctx.getStore() all valid).\n *\n * @param options.delayBeforeDrainMs — If set, wait this many ms after routes are ready before draining.\n * Use for timer (or other deferred) sources so at least one message is processed before drain/stop.\n */\n async test(options?: TestOptions): Promise<void> {\n const ctx = this.ctx;\n const total = ctx.getRoutes().length;\n const allReady =\n total === 0\n ? Promise.resolve()\n : new Promise<void>((resolve, reject) => {\n let ready = 0;\n let settled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined =\n setTimeout(() => {\n if (settled) return;\n cleanup();\n reject(new Error(\"Timeout waiting for routes to start\"));\n }, this.routesReadyTimeoutMs);\n\n const offRouteStarted = ctx.on(\"routeStarted\", () => {\n if (settled) return;\n ready++;\n if (ready >= total) {\n cleanup();\n resolve();\n }\n });\n const offError = ctx.on(\"error\", (payload) => {\n if (settled) return;\n cleanup();\n reject(payload.details.error);\n });\n\n function cleanup(): void {\n if (settled) return;\n settled = true;\n offRouteStarted();\n offError();\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n timeoutId = undefined;\n }\n }\n });\n const started = ctx.start();\n try {\n await allReady;\n const delayMs = options?.delayBeforeDrainMs ?? 0;\n if (delayMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n await ctx.drain();\n } finally {\n try {\n await ctx.stop();\n await started;\n } finally {\n this.restoreLoggerChild?.();\n }\n }\n }\n\n drain(): Promise<void> {\n return this.ctx.drain();\n }\n\n async stop(): Promise<void> {\n await this.ctx.stop();\n if (this.startedPromise !== undefined) {\n await this.startedPromise;\n }\n }\n}\n\n/**\n * Builder that returns TestContext instead of CraftContext.\n * Same API as ContextBuilder (routes, on, with, store).\n */\nexport class TestContextBuilder {\n private builder = new ContextBuilder();\n private routesReadyTimeoutMs: number | undefined;\n\n /** Override timeout for waiting for routes to start (ms). Used by tests that assert timeout behavior. */\n routesReadyTimeout(ms: number): this {\n this.routesReadyTimeoutMs = ms;\n return this;\n }\n\n with(config: CraftConfig): this {\n this.builder.with(config);\n return this;\n }\n\n on<K extends EventName>(event: K, handler: EventHandler<K>): this {\n this.builder.on(event, handler);\n return this;\n }\n\n store<K extends keyof StoreRegistry>(key: K, value: StoreRegistry[K]): this {\n this.builder.store(key, value);\n return this;\n }\n\n routes(\n routes:\n | RouteDefinition[]\n | RouteBuilder<unknown>[]\n | RouteDefinition\n | RouteBuilder<unknown>,\n ): this {\n this.builder.routes(routes);\n return this;\n }\n\n async build(): Promise<TestContext> {\n const spyLogger = createSpyLogger();\n const originalChild = logger.child.bind(logger);\n logger.child = vi.fn(\n () => spyLogger as unknown as ReturnType<typeof logger.child>,\n ) as typeof logger.child;\n const ctx = await this.builder.build();\n const options: TestContextOptions & {\n spyLogger: SpyLogger;\n restoreLoggerChild: () => void;\n } = {\n ...(this.routesReadyTimeoutMs !== undefined\n ? { routesReadyTimeoutMs: this.routesReadyTimeoutMs }\n : {}),\n spyLogger,\n restoreLoggerChild: () => {\n logger.child = originalChild;\n },\n };\n return new TestContext(ctx, options);\n }\n}\n\n/**\n * Create a test context builder. Use .routes(...).build(), await the result, then await t.test().\n *\n * @example\n * const builder = testContext();\n * const t = await builder.routes(myRoutes).build();\n * await t.test();\n */\nexport function testContext(): TestContextBuilder {\n return new TestContextBuilder();\n}\n\n/** Duck-type: object with send(exchange) returning Promise. */\nfunction isDestination(obj: unknown): obj is Destination<unknown, unknown> {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n typeof (obj as { send?: unknown }).send === \"function\"\n );\n}\n\n/**\n * Invoke a route by id or send to a destination and return the result.\n *\n * - **By route id:** Pass a string. The route is looked up with ctx.getRouteById(routeId).\n * The route's source must implement Destination (e.g. DirectAdapter). Works with multiple\n * routes in the default export — use the route's id.\n *\n * - **By destination:** Pass a Destination instance (e.g. direct(\"endpoint\")). Builds an\n * exchange and calls destination.send(exchange).\n *\n * @param ctx CraftContext (e.g. t.ctx from TestContext) with routes started\n * @param routeIdOrDestination Route id string or a Destination adapter instance\n * @param body Request body\n * @param headers Optional headers for the exchange\n * @returns The result from the route or destination (e.g. response body for DirectAdapter)\n */\nexport async function invoke<T = unknown, R = T>(\n ctx: CraftContext,\n routeIdOrDestination: string | Destination<T, R>,\n body: T,\n headers?: ExchangeHeaders,\n): Promise<R> {\n const exchange = new DefaultExchange(ctx, {\n body,\n ...(headers !== undefined && { headers }),\n });\n let dest: Destination<T, R>;\n\n if (typeof routeIdOrDestination === \"string\") {\n const route = ctx.getRouteById(routeIdOrDestination);\n if (!route) {\n throw new Error(\n `No route with id \"${routeIdOrDestination}\". Did you start the context (e.g. await t.test())?`,\n );\n }\n const source = route.definition.source;\n if (!isDestination(source)) {\n throw new Error(\n `Route \"${routeIdOrDestination}\" is not invokable: source must implement Destination (e.g. direct adapter).`,\n );\n }\n dest = source as Destination<T, R>;\n } else {\n dest = routeIdOrDestination;\n }\n\n const result = await dest.send(exchange as Parameters<typeof dest.send>[0]);\n return result as R;\n}\n\n/**\n * Load a JSON fixture file and return the parsed value.\n *\n * @param path Absolute or relative path to the JSON file\n * @returns Parsed JSON as T\n */\nexport function fixture<T = unknown>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\n\n/** Fixture entry must have a `name` field used as the vitest test name. */\nexport interface FixtureWithName {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Load a JSON array fixture and run one vitest test per entry. Each entry must have a `name` field (used as the test name).\n *\n * @param path Path to a JSON file that parses to an array\n * @param run Callback invoked per entry; use for assertions. Receives the fixture entry.\n */\nexport function fixtureEach<T extends FixtureWithName>(\n path: string,\n run: (entry: T) => void | Promise<void>,\n): void {\n const entries = fixture<T[]>(path);\n if (!Array.isArray(entries)) {\n throw new Error(\n `fixture.each: expected JSON array at \"${path}\", got ${typeof entries}`,\n );\n }\n for (const entry of entries) {\n if (typeof entry?.name !== \"string\") {\n throw new Error(\n `fixture.each: each entry must have a \"name\" field (string). Got: ${JSON.stringify(entry)}`,\n );\n }\n test(entry.name, () => run(entry));\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routecraft/testing",
|
|
3
|
-
"version": "0.3.0-canary.
|
|
3
|
+
"version": "0.3.0-canary.4",
|
|
4
4
|
"description": "Test utilities for RouteCraft routes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "pnpm run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@routecraft/routecraft": "^0.3.0-canary.
|
|
26
|
+
"@routecraft/routecraft": "^0.3.0-canary.4"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"vitest": "^4.0.18"
|