@navios/di 0.7.1 → 0.9.0

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.
Files changed (263) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +117 -17
  3. package/lib/browser/container/abstract-container.d.mts +112 -0
  4. package/lib/browser/container/abstract-container.d.mts.map +1 -0
  5. package/lib/browser/container/abstract-container.mjs +100 -0
  6. package/lib/browser/container/abstract-container.mjs.map +1 -0
  7. package/lib/browser/container/container.d.mts +100 -0
  8. package/lib/browser/container/container.d.mts.map +1 -0
  9. package/lib/browser/container/container.mjs +424 -0
  10. package/lib/browser/container/container.mjs.map +1 -0
  11. package/lib/browser/container/scoped-container.d.mts +93 -0
  12. package/lib/browser/container/scoped-container.d.mts.map +1 -0
  13. package/lib/browser/container/scoped-container.mjs +119 -0
  14. package/lib/browser/container/scoped-container.mjs.map +1 -0
  15. package/lib/browser/decorators/factory.decorator.d.mts +26 -0
  16. package/lib/browser/decorators/factory.decorator.d.mts.map +1 -0
  17. package/lib/browser/decorators/factory.decorator.mjs +20 -0
  18. package/lib/browser/decorators/factory.decorator.mjs.map +1 -0
  19. package/lib/browser/decorators/injectable.decorator.d.mts +38 -0
  20. package/lib/browser/decorators/injectable.decorator.d.mts.map +1 -0
  21. package/lib/browser/decorators/injectable.decorator.mjs +21 -0
  22. package/lib/browser/decorators/injectable.decorator.mjs.map +1 -0
  23. package/lib/browser/enums/injectable-scope.enum.d.mts +18 -0
  24. package/lib/browser/enums/injectable-scope.enum.d.mts.map +1 -0
  25. package/lib/browser/enums/injectable-scope.enum.mjs +20 -0
  26. package/lib/browser/enums/injectable-scope.enum.mjs.map +1 -0
  27. package/lib/browser/enums/injectable-type.enum.d.mts +8 -0
  28. package/lib/browser/enums/injectable-type.enum.d.mts.map +1 -0
  29. package/lib/browser/enums/injectable-type.enum.mjs +10 -0
  30. package/lib/browser/enums/injectable-type.enum.mjs.map +1 -0
  31. package/lib/browser/errors/di-error.d.mts +43 -0
  32. package/lib/browser/errors/di-error.d.mts.map +1 -0
  33. package/lib/browser/errors/di-error.mjs +98 -0
  34. package/lib/browser/errors/di-error.mjs.map +1 -0
  35. package/lib/browser/event-emitter.d.mts +16 -0
  36. package/lib/browser/event-emitter.d.mts.map +1 -0
  37. package/lib/browser/event-emitter.mjs +320 -0
  38. package/lib/browser/event-emitter.mjs.map +1 -0
  39. package/lib/browser/index.d.mts +37 -1508
  40. package/lib/browser/index.mjs +29 -2650
  41. package/lib/browser/interfaces/container.interface.d.mts +59 -0
  42. package/lib/browser/interfaces/container.interface.d.mts.map +1 -0
  43. package/lib/browser/interfaces/factory.interface.d.mts +14 -0
  44. package/lib/browser/interfaces/factory.interface.d.mts.map +1 -0
  45. package/lib/browser/interfaces/on-service-destroy.interface.d.mts +7 -0
  46. package/lib/browser/interfaces/on-service-destroy.interface.d.mts.map +1 -0
  47. package/lib/browser/interfaces/on-service-init.interface.d.mts +7 -0
  48. package/lib/browser/interfaces/on-service-init.interface.d.mts.map +1 -0
  49. package/lib/browser/internal/context/async-local-storage.browser.mjs +20 -0
  50. package/lib/browser/internal/context/async-local-storage.browser.mjs.map +1 -0
  51. package/lib/browser/internal/context/async-local-storage.d.mts +9 -0
  52. package/lib/browser/internal/context/async-local-storage.d.mts.map +1 -0
  53. package/lib/browser/internal/context/async-local-storage.types.d.mts +11 -0
  54. package/lib/browser/internal/context/async-local-storage.types.d.mts.map +1 -0
  55. package/lib/browser/internal/context/factory-context.d.mts +23 -0
  56. package/lib/browser/internal/context/factory-context.d.mts.map +1 -0
  57. package/lib/browser/internal/context/resolution-context.d.mts +43 -0
  58. package/lib/browser/internal/context/resolution-context.d.mts.map +1 -0
  59. package/lib/browser/internal/context/resolution-context.mjs +56 -0
  60. package/lib/browser/internal/context/resolution-context.mjs.map +1 -0
  61. package/lib/browser/internal/context/service-initialization-context.d.mts +48 -0
  62. package/lib/browser/internal/context/service-initialization-context.d.mts.map +1 -0
  63. package/lib/browser/internal/context/sync-local-storage.mjs +53 -0
  64. package/lib/browser/internal/context/sync-local-storage.mjs.map +1 -0
  65. package/lib/browser/internal/core/instance-resolver.d.mts +119 -0
  66. package/lib/browser/internal/core/instance-resolver.d.mts.map +1 -0
  67. package/lib/browser/internal/core/instance-resolver.mjs +306 -0
  68. package/lib/browser/internal/core/instance-resolver.mjs.map +1 -0
  69. package/lib/browser/internal/core/name-resolver.d.mts +52 -0
  70. package/lib/browser/internal/core/name-resolver.d.mts.map +1 -0
  71. package/lib/browser/internal/core/name-resolver.mjs +118 -0
  72. package/lib/browser/internal/core/name-resolver.mjs.map +1 -0
  73. package/lib/browser/internal/core/scope-tracker.d.mts +65 -0
  74. package/lib/browser/internal/core/scope-tracker.d.mts.map +1 -0
  75. package/lib/browser/internal/core/scope-tracker.mjs +120 -0
  76. package/lib/browser/internal/core/scope-tracker.mjs.map +1 -0
  77. package/lib/browser/internal/core/service-initializer.d.mts +44 -0
  78. package/lib/browser/internal/core/service-initializer.d.mts.map +1 -0
  79. package/lib/browser/internal/core/service-initializer.mjs +109 -0
  80. package/lib/browser/internal/core/service-initializer.mjs.map +1 -0
  81. package/lib/browser/internal/core/service-invalidator.d.mts +81 -0
  82. package/lib/browser/internal/core/service-invalidator.d.mts.map +1 -0
  83. package/lib/browser/internal/core/service-invalidator.mjs +142 -0
  84. package/lib/browser/internal/core/service-invalidator.mjs.map +1 -0
  85. package/lib/browser/internal/core/token-resolver.d.mts +54 -0
  86. package/lib/browser/internal/core/token-resolver.d.mts.map +1 -0
  87. package/lib/browser/internal/core/token-resolver.mjs +77 -0
  88. package/lib/browser/internal/core/token-resolver.mjs.map +1 -0
  89. package/lib/browser/internal/holder/holder-storage.interface.d.mts +99 -0
  90. package/lib/browser/internal/holder/holder-storage.interface.d.mts.map +1 -0
  91. package/lib/browser/internal/holder/instance-holder.d.mts +101 -0
  92. package/lib/browser/internal/holder/instance-holder.d.mts.map +1 -0
  93. package/lib/browser/internal/holder/instance-holder.mjs +19 -0
  94. package/lib/browser/internal/holder/instance-holder.mjs.map +1 -0
  95. package/lib/browser/internal/holder/unified-storage.d.mts +53 -0
  96. package/lib/browser/internal/holder/unified-storage.d.mts.map +1 -0
  97. package/lib/browser/internal/holder/unified-storage.mjs +144 -0
  98. package/lib/browser/internal/holder/unified-storage.mjs.map +1 -0
  99. package/lib/browser/internal/lifecycle/circular-detector.d.mts +39 -0
  100. package/lib/browser/internal/lifecycle/circular-detector.d.mts.map +1 -0
  101. package/lib/browser/internal/lifecycle/circular-detector.mjs +55 -0
  102. package/lib/browser/internal/lifecycle/circular-detector.mjs.map +1 -0
  103. package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts +18 -0
  104. package/lib/browser/internal/lifecycle/lifecycle-event-bus.d.mts.map +1 -0
  105. package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs +43 -0
  106. package/lib/browser/internal/lifecycle/lifecycle-event-bus.mjs.map +1 -0
  107. package/lib/browser/internal/stub-factory-class.d.mts +14 -0
  108. package/lib/browser/internal/stub-factory-class.d.mts.map +1 -0
  109. package/lib/browser/internal/stub-factory-class.mjs +18 -0
  110. package/lib/browser/internal/stub-factory-class.mjs.map +1 -0
  111. package/lib/browser/symbols/injectable-token.d.mts +5 -0
  112. package/lib/browser/symbols/injectable-token.d.mts.map +1 -0
  113. package/lib/browser/symbols/injectable-token.mjs +6 -0
  114. package/lib/browser/symbols/injectable-token.mjs.map +1 -0
  115. package/lib/browser/token/injection-token.d.mts +55 -0
  116. package/lib/browser/token/injection-token.d.mts.map +1 -0
  117. package/lib/browser/token/injection-token.mjs +100 -0
  118. package/lib/browser/token/injection-token.mjs.map +1 -0
  119. package/lib/browser/token/registry.d.mts +37 -0
  120. package/lib/browser/token/registry.d.mts.map +1 -0
  121. package/lib/browser/token/registry.mjs +86 -0
  122. package/lib/browser/token/registry.mjs.map +1 -0
  123. package/lib/browser/utils/default-injectors.d.mts +12 -0
  124. package/lib/browser/utils/default-injectors.d.mts.map +1 -0
  125. package/lib/browser/utils/default-injectors.mjs +13 -0
  126. package/lib/browser/utils/default-injectors.mjs.map +1 -0
  127. package/lib/browser/utils/get-injectable-token.d.mts +9 -0
  128. package/lib/browser/utils/get-injectable-token.d.mts.map +1 -0
  129. package/lib/browser/utils/get-injectable-token.mjs +13 -0
  130. package/lib/browser/utils/get-injectable-token.mjs.map +1 -0
  131. package/lib/browser/utils/get-injectors.d.mts +55 -0
  132. package/lib/browser/utils/get-injectors.d.mts.map +1 -0
  133. package/lib/browser/utils/get-injectors.mjs +121 -0
  134. package/lib/browser/utils/get-injectors.mjs.map +1 -0
  135. package/lib/browser/utils/types.d.mts +23 -0
  136. package/lib/browser/utils/types.d.mts.map +1 -0
  137. package/lib/{container-Pb_Y4Z4x.mjs → container-8-z89TyQ.mjs} +1269 -1305
  138. package/lib/container-8-z89TyQ.mjs.map +1 -0
  139. package/lib/{container-BuAutHGg.d.mts → container-CNiqesCL.d.mts} +600 -569
  140. package/lib/container-CNiqesCL.d.mts.map +1 -0
  141. package/lib/{container-DnzgpfBe.cjs → container-CaY2fDuk.cjs} +1287 -1329
  142. package/lib/container-CaY2fDuk.cjs.map +1 -0
  143. package/lib/{container-oGTgX2iX.d.cts → container-D-0Ho3qL.d.cts} +601 -565
  144. package/lib/container-D-0Ho3qL.d.cts.map +1 -0
  145. package/lib/index.cjs +13 -15
  146. package/lib/index.cjs.map +1 -1
  147. package/lib/index.d.cts +58 -223
  148. package/lib/index.d.cts.map +1 -1
  149. package/lib/index.d.mts +62 -222
  150. package/lib/index.d.mts.map +1 -1
  151. package/lib/index.mjs +5 -6
  152. package/lib/index.mjs.map +1 -1
  153. package/lib/testing/index.cjs +569 -311
  154. package/lib/testing/index.cjs.map +1 -1
  155. package/lib/testing/index.d.cts +370 -41
  156. package/lib/testing/index.d.cts.map +1 -1
  157. package/lib/testing/index.d.mts +370 -41
  158. package/lib/testing/index.d.mts.map +1 -1
  159. package/lib/testing/index.mjs +568 -305
  160. package/lib/testing/index.mjs.map +1 -1
  161. package/package.json +2 -1
  162. package/src/__tests__/circular-detector.spec.mts +193 -0
  163. package/src/__tests__/concurrent.spec.mts +368 -0
  164. package/src/__tests__/container.spec.mts +32 -30
  165. package/src/__tests__/di-error.spec.mts +351 -0
  166. package/src/__tests__/e2e.browser.spec.mts +0 -4
  167. package/src/__tests__/e2e.spec.mts +10 -19
  168. package/src/__tests__/event-emitter.spec.mts +232 -109
  169. package/src/__tests__/get-injectors.spec.mts +250 -39
  170. package/src/__tests__/injection-token.spec.mts +293 -349
  171. package/src/__tests__/library-findings.spec.mts +8 -8
  172. package/src/__tests__/registry.spec.mts +358 -210
  173. package/src/__tests__/resolution-context.spec.mts +255 -0
  174. package/src/__tests__/scope-tracker.spec.mts +598 -0
  175. package/src/__tests__/scope-upgrade.spec.mts +808 -0
  176. package/src/__tests__/scoped-container.spec.mts +595 -0
  177. package/src/__tests__/test-container.spec.mts +293 -0
  178. package/src/__tests__/token-resolver.spec.mts +207 -0
  179. package/src/__tests__/unified-storage.spec.mts +535 -0
  180. package/src/__tests__/unit-test-container.spec.mts +405 -0
  181. package/src/__type-tests__/container.spec-d.mts +180 -0
  182. package/src/__type-tests__/factory.spec-d.mts +15 -3
  183. package/src/__type-tests__/inject.spec-d.mts +115 -20
  184. package/src/__type-tests__/injectable.spec-d.mts +69 -52
  185. package/src/__type-tests__/injection-token.spec-d.mts +176 -0
  186. package/src/__type-tests__/scoped-container.spec-d.mts +212 -0
  187. package/src/container/abstract-container.mts +327 -0
  188. package/src/container/container.mts +142 -170
  189. package/src/container/scoped-container.mts +126 -208
  190. package/src/decorators/factory.decorator.mts +16 -11
  191. package/src/decorators/injectable.decorator.mts +20 -16
  192. package/src/enums/index.mts +2 -2
  193. package/src/enums/injectable-scope.enum.mts +1 -0
  194. package/src/enums/injectable-type.enum.mts +1 -0
  195. package/src/errors/di-error.mts +96 -0
  196. package/src/event-emitter.mts +3 -27
  197. package/src/index.mts +6 -153
  198. package/src/interfaces/container.interface.mts +13 -0
  199. package/src/interfaces/factory.interface.mts +1 -1
  200. package/src/interfaces/index.mts +1 -1
  201. package/src/internal/context/async-local-storage.mts +3 -2
  202. package/src/internal/context/async-local-storage.types.mts +1 -0
  203. package/src/internal/context/factory-context.mts +1 -0
  204. package/src/internal/context/index.mts +3 -1
  205. package/src/internal/context/resolution-context.mts +1 -0
  206. package/src/internal/context/service-initialization-context.mts +43 -0
  207. package/src/internal/core/index.mts +5 -4
  208. package/src/internal/core/instance-resolver.mts +461 -292
  209. package/src/internal/core/name-resolver.mts +196 -0
  210. package/src/internal/core/scope-tracker.mts +242 -0
  211. package/src/internal/core/{instantiator.mts → service-initializer.mts} +51 -29
  212. package/src/internal/core/service-invalidator.mts +290 -0
  213. package/src/internal/core/{token-processor.mts → token-resolver.mts} +17 -88
  214. package/src/internal/holder/holder-storage.interface.mts +11 -5
  215. package/src/internal/holder/index.mts +2 -5
  216. package/src/internal/holder/instance-holder.mts +1 -3
  217. package/src/internal/holder/unified-storage.mts +245 -0
  218. package/src/internal/index.mts +2 -1
  219. package/src/internal/lifecycle/circular-detector.mts +1 -0
  220. package/src/internal/lifecycle/index.mts +1 -1
  221. package/src/internal/lifecycle/lifecycle-event-bus.mts +1 -0
  222. package/src/internal/stub-factory-class.mts +16 -0
  223. package/src/symbols/injectable-token.mts +3 -1
  224. package/src/testing/index.mts +2 -0
  225. package/src/testing/test-container.mts +546 -85
  226. package/src/testing/types.mts +117 -0
  227. package/src/testing/unit-test-container.mts +509 -0
  228. package/src/token/injection-token.mts +41 -4
  229. package/src/token/registry.mts +75 -9
  230. package/src/utils/default-injectors.mts +16 -0
  231. package/src/utils/get-injectable-token.mts +2 -3
  232. package/src/utils/get-injectors.mts +26 -15
  233. package/src/utils/index.mts +3 -1
  234. package/src/utils/types.mts +1 -0
  235. package/tsdown.config.mts +11 -1
  236. package/lib/browser/index.d.mts.map +0 -1
  237. package/lib/browser/index.mjs.map +0 -1
  238. package/lib/container-BuAutHGg.d.mts.map +0 -1
  239. package/lib/container-DnzgpfBe.cjs.map +0 -1
  240. package/lib/container-Pb_Y4Z4x.mjs.map +0 -1
  241. package/lib/container-oGTgX2iX.d.cts.map +0 -1
  242. package/src/__tests__/async-local-storage.browser.spec.mts +0 -166
  243. package/src/__tests__/async-local-storage.spec.mts +0 -333
  244. package/src/__tests__/errors.spec.mts +0 -87
  245. package/src/__tests__/factory.spec.mts +0 -137
  246. package/src/__tests__/injectable.spec.mts +0 -246
  247. package/src/__tests__/request-scope.spec.mts +0 -416
  248. package/src/__tests__/service-instantiator.spec.mts +0 -410
  249. package/src/__tests__/service-locator-event-bus.spec.mts +0 -242
  250. package/src/__tests__/service-locator-manager.spec.mts +0 -300
  251. package/src/__tests__/service-locator.spec.mts +0 -966
  252. package/src/__tests__/unified-api.spec.mts +0 -130
  253. package/src/browser.mts +0 -11
  254. package/src/injectors.mts +0 -18
  255. package/src/internal/context/request-context.mts +0 -214
  256. package/src/internal/core/invalidator.mts +0 -437
  257. package/src/internal/core/service-locator.mts +0 -202
  258. package/src/internal/holder/base-holder-manager.mts +0 -238
  259. package/src/internal/holder/holder-manager.mts +0 -85
  260. package/src/internal/holder/request-storage.mts +0 -134
  261. package/src/internal/holder/singleton-storage.mts +0 -105
  262. package/src/testing/README.md +0 -80
  263. package/src/testing/__tests__/test-container.spec.mts +0 -173
@@ -1,120 +1,581 @@
1
- import type { ClassType, InjectionToken } from '../token/injection-token.mjs'
2
- import type { Registry } from '../token/registry.mjs'
3
- import type { Injectors } from '../utils/index.mjs'
1
+ import type {
2
+ BindingBuilder,
3
+ DependencyGraph,
4
+ DependencyNode,
5
+ LifecycleRecord,
6
+ MethodCallRecord,
7
+ MockServiceStats,
8
+ TestContainerOptions,
9
+ } from './types.mjs'
4
10
 
5
11
  import { Container } from '../container/container.mjs'
6
- import { Injectable } from '../decorators/injectable.decorator.mjs'
7
12
  import { InjectableScope, InjectableType } from '../enums/index.mjs'
8
- import { globalRegistry } from '../token/registry.mjs'
9
- import { getInjectableToken } from '../utils/index.mjs'
13
+ import { InjectionToken } from '../token/injection-token.mjs'
14
+ import { globalRegistry, Registry } from '../token/registry.mjs'
15
+ import { getInjectableToken } from '../utils/get-injectable-token.mjs'
16
+ import { defaultInjectors } from '../utils/index.mjs'
17
+
18
+ type AnyToken = InjectionToken<any, any> | (new (...args: any[]) => any)
10
19
 
11
20
  /**
12
- * A binding builder for the TestContainer that allows chaining binding operations.
21
+ * TestContainer extends Container with testing utilities.
22
+ *
23
+ * Provides simple value/class binding for integration/e2e tests,
24
+ * plus assertion helpers and dependency graph inspection.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const container = new TestContainer()
29
+ *
30
+ * // Bind mock values
31
+ * container.bind(DatabaseToken).toValue(mockDatabase)
32
+ * container.bind(UserService).toClass(MockUserService)
33
+ *
34
+ * // Use container normally
35
+ * const service = await container.get(MyService)
36
+ *
37
+ * // Assert on container state
38
+ * container.expectResolved(MyService)
39
+ * container.expectSingleton(MyService)
40
+ * ```
13
41
  */
14
- export class TestBindingBuilder<T> {
15
- constructor(
16
- private readonly container: TestContainer,
17
- private readonly token: InjectionToken<T, any>,
18
- ) {}
42
+ export class TestContainer extends Container {
43
+ private readonly testRegistry: Registry
44
+ private readonly methodCalls = new Map<string, MethodCallRecord[]>()
45
+ private readonly lifecycleEvents = new Map<string, LifecycleRecord[]>()
46
+ private readonly instanceCounts = new Map<string, number>()
47
+ private readonly boundTokens = new Set<string>()
48
+
49
+ /**
50
+ * Creates a new TestContainer.
51
+ *
52
+ * @param options - Configuration options
53
+ * @param options.parentRegistry - Parent registry. Defaults to globalRegistry.
54
+ * Pass `null` for a completely isolated container.
55
+ * @param options.logger - Optional logger for debugging.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * // Uses globalRegistry as parent (default)
60
+ * const container = new TestContainer()
61
+ *
62
+ * // Isolated container (no access to @Injectable classes)
63
+ * const isolated = new TestContainer({ parentRegistry: null })
64
+ *
65
+ * // Custom parent registry
66
+ * const custom = new TestContainer({ parentRegistry: myRegistry })
67
+ * ```
68
+ */
69
+ constructor(options: TestContainerOptions = {}) {
70
+ const { parentRegistry = globalRegistry, logger = null } = options
71
+ const testRegistry = parentRegistry
72
+ ? new Registry(parentRegistry)
73
+ : new Registry()
74
+ super(testRegistry, logger, defaultInjectors)
75
+ this.testRegistry = testRegistry
76
+ }
77
+
78
+ // ============================================================================
79
+ // BINDING API
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Creates a binding builder for the given token.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * container.bind(UserService).toValue(mockUserService)
88
+ * container.bind(DatabaseToken).toClass(MockDatabase)
89
+ * container.bind(ConfigToken).toFactory(() => ({ apiKey: 'test' }))
90
+ * ```
91
+ */
92
+ bind<T>(
93
+ token: InjectionToken<T, any> | (new (...args: any[]) => T),
94
+ ): BindingBuilder<T> {
95
+ const realToken = this.resolveToken(token)
96
+ const tokenId = realToken.id
97
+
98
+ return {
99
+ toValue: (value: T) => {
100
+ this.boundTokens.add(tokenId)
101
+ this.registerValueBinding(realToken, value)
102
+ },
103
+ toClass: <C extends new (...args: any[]) => T>(cls: C) => {
104
+ this.boundTokens.add(tokenId)
105
+ this.registerClassBinding(realToken, cls)
106
+ },
107
+ toFactory: (factory: () => T | Promise<T>) => {
108
+ this.boundTokens.add(tokenId)
109
+ this.registerFactoryBinding(realToken, factory)
110
+ },
111
+ }
112
+ }
19
113
 
20
114
  /**
21
- * Binds the token to a specific value.
22
- * This is useful for testing with mock values or constants.
23
- * @param value The value to bind to the token
115
+ * Clears all bindings and resets container state.
24
116
  */
25
- toValue(value: T): TestContainer {
26
- const instanceName = this.container
27
- .getServiceLocator()
28
- .getInstanceIdentifier(this.token)
29
- this.container
30
- .getServiceLocator()
31
- .getManager()
32
- .storeCreatedHolder(
33
- instanceName,
34
- value,
35
- InjectableType.Class,
36
- InjectableScope.Singleton,
117
+ async clear(): Promise<void> {
118
+ await this.dispose()
119
+ this.methodCalls.clear()
120
+ this.lifecycleEvents.clear()
121
+ this.instanceCounts.clear()
122
+ this.boundTokens.clear()
123
+ }
124
+
125
+ // ============================================================================
126
+ // ASSERTION HELPERS
127
+ // ============================================================================
128
+
129
+ /**
130
+ * Asserts that a service has been resolved at least once.
131
+ */
132
+ expectResolved(token: AnyToken): void {
133
+ const realToken = this.resolveToken(token)
134
+ const storage = this.getStorage()
135
+ const names = storage.getAllNames()
136
+ const found = names.some((name) => name.includes(realToken.id))
137
+
138
+ if (!found) {
139
+ throw new Error(
140
+ `Expected ${realToken.toString()} to be resolved, but it was not`,
37
141
  )
38
- return this.container
142
+ }
39
143
  }
40
144
 
41
145
  /**
42
- * Binds the token to a class constructor.
43
- * @param target The class constructor to bind to
146
+ * Asserts that a service has NOT been resolved.
44
147
  */
45
- toClass(target: ClassType): TestContainer {
46
- this.container['registry'].set(
47
- this.token,
48
- InjectableScope.Singleton,
49
- target,
50
- InjectableType.Class,
148
+ expectNotResolved(token: AnyToken): void {
149
+ const realToken = this.resolveToken(token)
150
+ const storage = this.getStorage()
151
+ const names = storage.getAllNames()
152
+ const found = names.some((name) => name.includes(realToken.id))
153
+
154
+ if (found) {
155
+ throw new Error(
156
+ `Expected ${realToken.toString()} to NOT be resolved, but it was`,
157
+ )
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Asserts that a service is registered as singleton scope.
163
+ */
164
+ expectSingleton(token: AnyToken): void {
165
+ const realToken = this.resolveToken(token)
166
+ const registry = this.getRegistry()
167
+
168
+ if (!registry.has(realToken)) {
169
+ throw new Error(
170
+ `Expected ${realToken.toString()} to be registered, but it was not`,
171
+ )
172
+ }
173
+
174
+ const record = registry.get(realToken)
175
+ if (record.scope !== InjectableScope.Singleton) {
176
+ throw new Error(
177
+ `Expected ${realToken.toString()} to be Singleton scope, but it was ${record.scope}`,
178
+ )
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Asserts that a service is registered as transient scope.
184
+ */
185
+ expectTransient(token: AnyToken): void {
186
+ const realToken = this.resolveToken(token)
187
+ const registry = this.getRegistry()
188
+
189
+ if (!registry.has(realToken)) {
190
+ throw new Error(
191
+ `Expected ${realToken.toString()} to be registered, but it was not`,
192
+ )
193
+ }
194
+
195
+ const record = registry.get(realToken)
196
+ if (record.scope !== InjectableScope.Transient) {
197
+ throw new Error(
198
+ `Expected ${realToken.toString()} to be Transient scope, but it was ${record.scope}`,
199
+ )
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Asserts that a service is registered as request scope.
205
+ */
206
+ expectRequestScoped(token: AnyToken): void {
207
+ const realToken = this.resolveToken(token)
208
+ const registry = this.getRegistry()
209
+
210
+ if (!registry.has(realToken)) {
211
+ throw new Error(
212
+ `Expected ${realToken.toString()} to be registered, but it was not`,
213
+ )
214
+ }
215
+
216
+ const record = registry.get(realToken)
217
+ if (record.scope !== InjectableScope.Request) {
218
+ throw new Error(
219
+ `Expected ${realToken.toString()} to be Request scope, but it was ${record.scope}`,
220
+ )
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Asserts that two service resolutions return the same instance.
226
+ */
227
+ async expectSameInstance(token: AnyToken): Promise<void> {
228
+ const instance1 = await this.get(token as any)
229
+ const instance2 = await this.get(token as any)
230
+
231
+ if (instance1 !== instance2) {
232
+ const realToken = this.resolveToken(token)
233
+ throw new Error(
234
+ `Expected ${realToken.toString()} to return same instance, but got different instances`,
235
+ )
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Asserts that two service resolutions return different instances.
241
+ */
242
+ async expectDifferentInstances(token: AnyToken): Promise<void> {
243
+ const instance1 = await this.get(token as any)
244
+ const instance2 = await this.get(token as any)
245
+
246
+ if (instance1 === instance2) {
247
+ const realToken = this.resolveToken(token)
248
+ throw new Error(
249
+ `Expected ${realToken.toString()} to return different instances, but got same instance`,
250
+ )
251
+ }
252
+ }
253
+
254
+ // ============================================================================
255
+ // LIFECYCLE ASSERTIONS
256
+ // ============================================================================
257
+
258
+ /**
259
+ * Asserts that a service's onServiceInit was called.
260
+ */
261
+ expectInitialized(token: AnyToken): void {
262
+ const realToken = this.resolveToken(token)
263
+ const events = this.lifecycleEvents.get(realToken.id) || []
264
+ const initialized = events.some((e) => e.event === 'initialized')
265
+
266
+ if (!initialized) {
267
+ throw new Error(
268
+ `Expected ${realToken.toString()} to be initialized, but onServiceInit was not called`,
269
+ )
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Asserts that a service's onServiceDestroy was called.
275
+ */
276
+ expectDestroyed(token: AnyToken): void {
277
+ const realToken = this.resolveToken(token)
278
+ const events = this.lifecycleEvents.get(realToken.id) || []
279
+ const destroyed = events.some((e) => e.event === 'destroyed')
280
+
281
+ if (!destroyed) {
282
+ throw new Error(
283
+ `Expected ${realToken.toString()} to be destroyed, but onServiceDestroy was not called`,
284
+ )
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Asserts that a service has NOT been destroyed.
290
+ */
291
+ expectNotDestroyed(token: AnyToken): void {
292
+ const realToken = this.resolveToken(token)
293
+ const events = this.lifecycleEvents.get(realToken.id) || []
294
+ const destroyed = events.some((e) => e.event === 'destroyed')
295
+
296
+ if (destroyed) {
297
+ throw new Error(
298
+ `Expected ${realToken.toString()} to NOT be destroyed, but onServiceDestroy was called`,
299
+ )
300
+ }
301
+ }
302
+
303
+ // ============================================================================
304
+ // CALL TRACKING
305
+ // ============================================================================
306
+
307
+ /**
308
+ * Records a method call for tracking.
309
+ * Call this from your mock implementations.
310
+ */
311
+ recordMethodCall(
312
+ token: AnyToken,
313
+ method: string,
314
+ args: unknown[],
315
+ result?: unknown,
316
+ error?: Error,
317
+ ): void {
318
+ const realToken = this.resolveToken(token)
319
+ const calls = this.methodCalls.get(realToken.id) || []
320
+ calls.push({
321
+ method,
322
+ args,
323
+ timestamp: Date.now(),
324
+ result,
325
+ error,
326
+ })
327
+ this.methodCalls.set(realToken.id, calls)
328
+ }
329
+
330
+ /**
331
+ * Records a lifecycle event for tracking.
332
+ */
333
+ recordLifecycleEvent(
334
+ token: AnyToken,
335
+ event: 'created' | 'initialized' | 'destroyed',
336
+ instanceName: string,
337
+ ): void {
338
+ const realToken = this.resolveToken(token)
339
+ const events = this.lifecycleEvents.get(realToken.id) || []
340
+ events.push({
341
+ event,
342
+ timestamp: Date.now(),
343
+ instanceName,
344
+ })
345
+ this.lifecycleEvents.set(realToken.id, events)
346
+
347
+ if (event === 'created') {
348
+ const count = this.instanceCounts.get(realToken.id) || 0
349
+ this.instanceCounts.set(realToken.id, count + 1)
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Asserts that a method was called on a service.
355
+ */
356
+ expectCalled(token: AnyToken, method: string): void {
357
+ const realToken = this.resolveToken(token)
358
+ const calls = this.methodCalls.get(realToken.id) || []
359
+ const found = calls.some((c) => c.method === method)
360
+
361
+ if (!found) {
362
+ throw new Error(
363
+ `Expected ${realToken.toString()}.${method}() to be called, but it was not`,
364
+ )
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Asserts that a method was called with specific arguments.
370
+ */
371
+ expectCalledWith(
372
+ token: AnyToken,
373
+ method: string,
374
+ expectedArgs: unknown[],
375
+ ): void {
376
+ const realToken = this.resolveToken(token)
377
+ const calls = this.methodCalls.get(realToken.id) || []
378
+ const found = calls.some(
379
+ (c) => c.method === method && this.argsMatch(c.args, expectedArgs),
51
380
  )
52
- return this.container
381
+
382
+ if (!found) {
383
+ throw new Error(
384
+ `Expected ${realToken.toString()}.${method}() to be called with ${JSON.stringify(expectedArgs)}, but it was not`,
385
+ )
386
+ }
53
387
  }
54
- }
55
388
 
56
- /**
57
- * TestContainer extends the base Container with additional methods useful for testing.
58
- * It provides a simplified API for binding values and classes during test setup.
59
- */
60
- @Injectable()
61
- export class TestContainer extends Container {
62
- constructor(
63
- registry: Registry = globalRegistry,
64
- logger: Console | null = null,
65
- injectors: Injectors = undefined as any,
66
- ) {
67
- super(registry, logger, injectors)
389
+ /**
390
+ * Asserts that a method was called a specific number of times.
391
+ */
392
+ expectCallCount(token: AnyToken, method: string, count: number): void {
393
+ const realToken = this.resolveToken(token)
394
+ const calls = this.methodCalls.get(realToken.id) || []
395
+ const actualCount = calls.filter((c) => c.method === method).length
396
+
397
+ if (actualCount !== count) {
398
+ throw new Error(
399
+ `Expected ${realToken.toString()}.${method}() to be called ${count} times, but was called ${actualCount} times`,
400
+ )
401
+ }
68
402
  }
69
403
 
70
404
  /**
71
- * Creates a binding builder for the given token.
72
- * This allows chaining binding operations like bind(Token).toValue(value).
73
- * @param token The injection token to bind
74
- * @returns A TestBindingBuilder for chaining binding operations
75
- */
76
- bind<T>(token: ClassType): TestBindingBuilder<T>
77
- bind<T>(token: InjectionToken<T, any>): TestBindingBuilder<T>
78
- bind(token: any): TestBindingBuilder<any> {
79
- let realToken = token
80
- if (typeof token === 'function') {
81
- realToken = getInjectableToken(token)
405
+ * Gets all recorded method calls for a service.
406
+ */
407
+ getMethodCalls(token: AnyToken): MethodCallRecord[] {
408
+ const realToken = this.resolveToken(token)
409
+ return this.methodCalls.get(realToken.id) || []
410
+ }
411
+
412
+ /**
413
+ * Gets statistics about a mocked service.
414
+ */
415
+ getServiceStats(token: AnyToken): MockServiceStats {
416
+ const realToken = this.resolveToken(token)
417
+ return {
418
+ instanceCount: this.instanceCounts.get(realToken.id) || 0,
419
+ methodCalls: this.methodCalls.get(realToken.id) || [],
420
+ lifecycleEvents: this.lifecycleEvents.get(realToken.id) || [],
82
421
  }
83
- return new TestBindingBuilder(this, realToken)
84
422
  }
85
423
 
86
424
  /**
87
- * Binds a value directly to a token.
88
- * This is a convenience method equivalent to bind(token).toValue(value).
89
- * @param token The injection token to bind
90
- * @param value The value to bind to the token
91
- * @returns The TestContainer instance for chaining
425
+ * Clears all recorded method calls.
92
426
  */
93
- bindValue<T>(token: ClassType, value: T): TestContainer
94
- bindValue<T>(token: InjectionToken<T, any>, value: T): TestContainer
95
- bindValue(token: any, value: any): TestContainer {
96
- return this.bind(token).toValue(value)
427
+ clearMethodCalls(): void {
428
+ this.methodCalls.clear()
97
429
  }
98
430
 
431
+ // ============================================================================
432
+ // DEPENDENCY GRAPH
433
+ // ============================================================================
434
+
99
435
  /**
100
- * Binds a class to a token.
101
- * This is a convenience method equivalent to bind(token).toClass(target).
102
- * @param token The injection token to bind
103
- * @param target The class constructor to bind to
104
- * @returns The TestContainer instance for chaining
436
+ * Gets the dependency graph for snapshot testing.
437
+ * Returns a serializable structure that can be used with vitest snapshots.
105
438
  */
106
- bindClass(token: ClassType, target: ClassType): TestContainer
107
- bindClass<T>(token: InjectionToken<T, any>, target: ClassType): TestContainer
108
- bindClass(token: any, target: any): TestContainer {
109
- return this.bind(token).toClass(target)
439
+ getDependencyGraph(): DependencyGraph {
440
+ const storage = this.getStorage()
441
+ const nodes: Record<string, DependencyNode> = {}
442
+ const rootTokens: string[] = []
443
+
444
+ storage.forEach((name, holder) => {
445
+ const tokenMatch = name.match(/^([^:]+)/)
446
+ const tokenId = tokenMatch ? tokenMatch[1] : name
447
+
448
+ nodes[name] = {
449
+ token: tokenId,
450
+ instanceName: name,
451
+ scope: holder.scope,
452
+ dependencies: Array.from(holder.deps),
453
+ dependents: storage.findDependents(name),
454
+ }
455
+
456
+ // Root tokens have no dependents
457
+ if (storage.findDependents(name).length === 0) {
458
+ rootTokens.push(name)
459
+ }
460
+ })
461
+
462
+ return { nodes, rootTokens }
110
463
  }
111
464
 
112
465
  /**
113
- * Creates a new TestContainer instance with the same configuration.
114
- * This is useful for creating isolated test containers.
115
- * @returns A new TestContainer instance
466
+ * Gets a simplified dependency graph showing only token relationships.
467
+ * Useful for cleaner snapshot comparisons.
116
468
  */
117
- createChild(): TestContainer {
118
- return new TestContainer(this.registry, this.logger, this.injectors)
469
+ getSimplifiedDependencyGraph(): Record<string, string[]> {
470
+ const storage = this.getStorage()
471
+ const graph: Record<string, string[]> = {}
472
+
473
+ storage.forEach((name, holder) => {
474
+ const tokenMatch = name.match(/^([^:]+)/)
475
+ const tokenId = tokenMatch ? tokenMatch[1] : name
476
+
477
+ if (!graph[tokenId]) {
478
+ graph[tokenId] = []
479
+ }
480
+
481
+ for (const dep of holder.deps) {
482
+ const depTokenMatch = dep.match(/^([^:]+)/)
483
+ const depTokenId = depTokenMatch ? depTokenMatch[1] : dep
484
+ if (!graph[tokenId].includes(depTokenId)) {
485
+ graph[tokenId].push(depTokenId)
486
+ }
487
+ }
488
+ })
489
+
490
+ // Sort for consistent snapshots
491
+ for (const key of Object.keys(graph)) {
492
+ graph[key].sort()
493
+ }
494
+
495
+ return graph
496
+ }
497
+
498
+ // ============================================================================
499
+ // INTERNAL HELPERS
500
+ // ============================================================================
501
+
502
+ private resolveToken(token: AnyToken): InjectionToken<any, any> {
503
+ if (typeof token === 'function') {
504
+ return getInjectableToken(token)
505
+ }
506
+ return token
507
+ }
508
+
509
+ private registerValueBinding<T>(
510
+ token: InjectionToken<T, any>,
511
+ value: T,
512
+ ): void {
513
+ // Create a simple class that returns the value
514
+ const ValueHolder = class {
515
+ static instance = value
516
+ }
517
+
518
+ this.testRegistry.set(
519
+ token,
520
+ InjectableScope.Singleton,
521
+ ValueHolder,
522
+ InjectableType.Class,
523
+ )
524
+
525
+ // Store the instance directly
526
+ const nameResolver = this.getNameResolver()
527
+ const instanceName = nameResolver.generateInstanceName(
528
+ token,
529
+ undefined,
530
+ undefined,
531
+ InjectableScope.Singleton,
532
+ )
533
+ this.getStorage().storeInstance(instanceName, value)
534
+ this.recordLifecycleEvent(token, 'created', instanceName)
535
+ }
536
+
537
+ private registerClassBinding<T>(
538
+ token: InjectionToken<T, any>,
539
+ cls: new (...args: any[]) => T,
540
+ ): void {
541
+ this.testRegistry.set(
542
+ token,
543
+ InjectableScope.Singleton,
544
+ cls,
545
+ InjectableType.Class,
546
+ )
547
+ }
548
+
549
+ private registerFactoryBinding<T>(
550
+ token: InjectionToken<T, any>,
551
+ factory: () => T | Promise<T>,
552
+ ): void {
553
+ // Create a factory class wrapper
554
+ const FactoryWrapper = class {
555
+ static factory = factory
556
+ async create(): Promise<T> {
557
+ return await factory()
558
+ }
559
+ }
560
+
561
+ this.testRegistry.set(
562
+ token,
563
+ InjectableScope.Singleton,
564
+ FactoryWrapper,
565
+ InjectableType.Factory,
566
+ )
567
+ }
568
+
569
+ private argsMatch(actual: unknown[], expected: unknown[]): boolean {
570
+ if (actual.length !== expected.length) {
571
+ return false
572
+ }
573
+ return actual.every((arg, index) => {
574
+ const exp = expected[index]
575
+ if (typeof exp === 'object' && exp !== null) {
576
+ return JSON.stringify(arg) === JSON.stringify(exp)
577
+ }
578
+ return arg === exp
579
+ })
119
580
  }
120
581
  }