velocious 1.0.643 → 1.0.645

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 (279) hide show
  1. package/README.md +21 -10
  2. package/build/authorization/ability.js +36 -13
  3. package/build/background-jobs/adapter.js +9 -0
  4. package/build/background-jobs/main.js +35 -10
  5. package/build/background-jobs/store.js +217 -38
  6. package/build/background-jobs/types.js +15 -0
  7. package/build/configuration-types.js +1 -1
  8. package/build/configuration.js +45 -9
  9. package/build/database/drivers/base-column.js +8 -0
  10. package/build/database/drivers/mssql/sql/update.js +1 -3
  11. package/build/database/drivers/mysql/sql/update.js +1 -3
  12. package/build/database/drivers/pgsql/column.js +22 -0
  13. package/build/database/drivers/pgsql/sql/update.js +1 -3
  14. package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
  15. package/build/database/drivers/sqlite/sql/update.js +1 -3
  16. package/build/database/query/index.js +2 -2
  17. package/build/database/query/model-class-query.js +26 -10
  18. package/build/database/query/preloader/has-many.js +4 -1
  19. package/build/database/query/query-data.js +2 -1
  20. package/build/database/query/update-base.js +14 -0
  21. package/build/database/query/with-count.js +3 -1
  22. package/build/database/record/acts-as-list.js +37 -13
  23. package/build/database/record/attachments/store.js +336 -26
  24. package/build/database/record/auditing.js +6 -3
  25. package/build/database/record/index.js +197 -42
  26. package/build/database/record/instance-relationships/belongs-to.js +2 -1
  27. package/build/database/record/validators/uniqueness.js +8 -5
  28. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  29. package/build/frontend-model-controller.js +136 -53
  30. package/build/frontend-model-resource/base-resource.js +87 -37
  31. package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
  32. package/build/frontend-models/base.js +307 -59
  33. package/build/frontend-models/preloader.js +6 -3
  34. package/build/frontend-models/query.js +15 -6
  35. package/build/frontend-models/resource-definition.js +21 -2
  36. package/build/frontend-models/use-destroyed-event.js +1 -1
  37. package/build/frontend-models/use-model-class-event.js +2 -2
  38. package/build/frontend-models/use-updated-event.js +1 -1
  39. package/build/frontend-models/websocket-channel.js +256 -93
  40. package/build/frontend-models/websocket-publishers.js +218 -18
  41. package/build/http-server/client/websocket-session.js +13 -1
  42. package/build/http-server/websocket-channel.js +15 -2
  43. package/build/src/authorization/ability.d.ts +9 -3
  44. package/build/src/authorization/ability.d.ts.map +1 -1
  45. package/build/src/authorization/ability.js +33 -14
  46. package/build/src/background-jobs/adapter.d.ts +6 -0
  47. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  48. package/build/src/background-jobs/adapter.js +9 -1
  49. package/build/src/background-jobs/main.d.ts +6 -0
  50. package/build/src/background-jobs/main.d.ts.map +1 -1
  51. package/build/src/background-jobs/main.js +34 -11
  52. package/build/src/background-jobs/store.d.ts +66 -12
  53. package/build/src/background-jobs/store.d.ts.map +1 -1
  54. package/build/src/background-jobs/store.js +191 -35
  55. package/build/src/background-jobs/types.d.ts +55 -0
  56. package/build/src/background-jobs/types.d.ts.map +1 -1
  57. package/build/src/background-jobs/types.js +16 -1
  58. package/build/src/configuration-types.d.ts +3 -3
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +2 -2
  61. package/build/src/configuration.d.ts +18 -8
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +38 -7
  64. package/build/src/database/drivers/base-column.d.ts +5 -0
  65. package/build/src/database/drivers/base-column.d.ts.map +1 -1
  66. package/build/src/database/drivers/base-column.js +8 -1
  67. package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
  68. package/build/src/database/drivers/mssql/sql/update.js +2 -4
  69. package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
  70. package/build/src/database/drivers/mysql/sql/update.js +2 -4
  71. package/build/src/database/drivers/pgsql/column.d.ts +5 -0
  72. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  73. package/build/src/database/drivers/pgsql/column.js +19 -1
  74. package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
  75. package/build/src/database/drivers/pgsql/sql/update.js +2 -4
  76. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  77. package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
  78. package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
  79. package/build/src/database/drivers/sqlite/sql/update.js +2 -4
  80. package/build/src/database/query/index.d.ts +4 -4
  81. package/build/src/database/query/index.d.ts.map +1 -1
  82. package/build/src/database/query/index.js +3 -3
  83. package/build/src/database/query/model-class-query.d.ts +8 -2
  84. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  85. package/build/src/database/query/model-class-query.js +25 -11
  86. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  87. package/build/src/database/query/preloader/has-many.js +4 -2
  88. package/build/src/database/query/query-data.d.ts.map +1 -1
  89. package/build/src/database/query/query-data.js +3 -2
  90. package/build/src/database/query/update-base.d.ts +7 -0
  91. package/build/src/database/query/update-base.d.ts.map +1 -1
  92. package/build/src/database/query/update-base.js +13 -1
  93. package/build/src/database/query/with-count.d.ts.map +1 -1
  94. package/build/src/database/query/with-count.js +3 -2
  95. package/build/src/database/record/acts-as-list.d.ts.map +1 -1
  96. package/build/src/database/record/acts-as-list.js +35 -14
  97. package/build/src/database/record/attachments/store.d.ts +67 -0
  98. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  99. package/build/src/database/record/attachments/store.js +301 -25
  100. package/build/src/database/record/auditing.d.ts.map +1 -1
  101. package/build/src/database/record/auditing.js +7 -4
  102. package/build/src/database/record/index.d.ts +78 -26
  103. package/build/src/database/record/index.d.ts.map +1 -1
  104. package/build/src/database/record/index.js +171 -42
  105. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  106. package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
  107. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  108. package/build/src/database/record/validators/uniqueness.js +8 -6
  109. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
  110. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
  112. package/build/src/frontend-model-controller.d.ts +29 -8
  113. package/build/src/frontend-model-controller.d.ts.map +1 -1
  114. package/build/src/frontend-model-controller.js +122 -51
  115. package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
  116. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  117. package/build/src/frontend-model-resource/base-resource.js +83 -37
  118. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
  119. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  120. package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
  121. package/build/src/frontend-models/base.d.ts +53 -20
  122. package/build/src/frontend-models/base.d.ts.map +1 -1
  123. package/build/src/frontend-models/base.js +275 -59
  124. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  125. package/build/src/frontend-models/preloader.js +6 -4
  126. package/build/src/frontend-models/query.d.ts +2 -2
  127. package/build/src/frontend-models/query.d.ts.map +1 -1
  128. package/build/src/frontend-models/query.js +14 -7
  129. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  130. package/build/src/frontend-models/resource-definition.js +20 -3
  131. package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
  132. package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
  133. package/build/src/frontend-models/use-destroyed-event.js +2 -2
  134. package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
  135. package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
  136. package/build/src/frontend-models/use-model-class-event.js +3 -3
  137. package/build/src/frontend-models/use-updated-event.d.ts +1 -1
  138. package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
  139. package/build/src/frontend-models/use-updated-event.js +2 -2
  140. package/build/src/frontend-models/websocket-channel.d.ts +60 -41
  141. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  142. package/build/src/frontend-models/websocket-channel.js +225 -85
  143. package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
  144. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  145. package/build/src/frontend-models/websocket-publishers.js +190 -19
  146. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  147. package/build/src/http-server/client/websocket-session.js +11 -2
  148. package/build/src/http-server/websocket-channel.d.ts +20 -8
  149. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  150. package/build/src/http-server/websocket-channel.js +15 -3
  151. package/build/src/sync/assets/cache.d.ts +288 -0
  152. package/build/src/sync/assets/cache.d.ts.map +1 -0
  153. package/build/src/sync/assets/cache.js +883 -0
  154. package/build/src/sync/assets/types.d.ts +199 -0
  155. package/build/src/sync/assets/types.d.ts.map +1 -0
  156. package/build/src/sync/assets/types.js +56 -0
  157. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  158. package/build/src/sync/sync-api-client.js +4 -3
  159. package/build/src/sync/sync-client.d.ts.map +1 -1
  160. package/build/src/sync/sync-client.js +3 -2
  161. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  162. package/build/src/sync/sync-envelope-replay-service.js +6 -5
  163. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  164. package/build/src/sync/sync-publisher.js +4 -3
  165. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
  166. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
  167. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
  168. package/build/src/testing/test-profiler.d.ts +4 -4
  169. package/build/src/testing/test-profiler.d.ts.map +1 -1
  170. package/build/src/testing/test-profiler.js +3 -3
  171. package/build/src/testing/test-runner.d.ts +279 -117
  172. package/build/src/testing/test-runner.d.ts.map +1 -1
  173. package/build/src/testing/test-runner.js +503 -933
  174. package/build/src/testing/test.d.ts +11 -72
  175. package/build/src/testing/test.d.ts.map +1 -1
  176. package/build/src/testing/test.js +131 -284
  177. package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
  178. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
  179. package/build/src/testing/velocious-attempt-executor.js +408 -0
  180. package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
  181. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
  182. package/build/src/testing/velocious-runner-reporter.js +240 -0
  183. package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
  184. package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
  185. package/build/src/testing/velocious-suite-hook-executor.js +41 -0
  186. package/build/src/testing/velocious-test-arguments.d.ts +39 -0
  187. package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
  188. package/build/src/testing/velocious-test-arguments.js +55 -0
  189. package/build/src/utils/model-primary-key.d.ts +73 -0
  190. package/build/src/utils/model-primary-key.d.ts.map +1 -0
  191. package/build/src/utils/model-primary-key.js +144 -0
  192. package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
  193. package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
  194. package/build/src/utils/sha256-bytes-hex.js +117 -0
  195. package/build/src/utils/sha256-hex.d.ts +2 -4
  196. package/build/src/utils/sha256-hex.d.ts.map +1 -1
  197. package/build/src/utils/sha256-hex.js +6 -120
  198. package/build/sync/assets/cache.js +998 -0
  199. package/build/sync/assets/types.js +62 -0
  200. package/build/sync/sync-api-client.js +3 -2
  201. package/build/sync/sync-client.js +2 -1
  202. package/build/sync/sync-envelope-replay-service.js +5 -4
  203. package/build/sync/sync-publisher.js +3 -2
  204. package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  205. package/build/testing/test-profiler.js +2 -2
  206. package/build/testing/test-runner.js +496 -990
  207. package/build/testing/test.js +164 -306
  208. package/build/testing/velocious-attempt-executor.js +441 -0
  209. package/build/testing/velocious-runner-reporter.js +264 -0
  210. package/build/testing/velocious-suite-hook-executor.js +43 -0
  211. package/build/testing/velocious-test-arguments.js +64 -0
  212. package/build/tsconfig.tsbuildinfo +1 -1
  213. package/build/utils/model-primary-key.js +171 -0
  214. package/build/utils/sha256-bytes-hex.js +132 -0
  215. package/build/utils/sha256-hex.js +7 -135
  216. package/package.json +5 -4
  217. package/src/authorization/ability.js +36 -13
  218. package/src/background-jobs/adapter.js +9 -0
  219. package/src/background-jobs/main.js +35 -10
  220. package/src/background-jobs/store.js +217 -38
  221. package/src/background-jobs/types.js +15 -0
  222. package/src/configuration-types.js +1 -1
  223. package/src/configuration.js +45 -9
  224. package/src/database/drivers/base-column.js +8 -0
  225. package/src/database/drivers/mssql/sql/update.js +1 -3
  226. package/src/database/drivers/mysql/sql/update.js +1 -3
  227. package/src/database/drivers/pgsql/column.js +22 -0
  228. package/src/database/drivers/pgsql/sql/update.js +1 -3
  229. package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
  230. package/src/database/drivers/sqlite/sql/update.js +1 -3
  231. package/src/database/query/index.js +2 -2
  232. package/src/database/query/model-class-query.js +26 -10
  233. package/src/database/query/preloader/has-many.js +4 -1
  234. package/src/database/query/query-data.js +2 -1
  235. package/src/database/query/update-base.js +14 -0
  236. package/src/database/query/with-count.js +3 -1
  237. package/src/database/record/acts-as-list.js +37 -13
  238. package/src/database/record/attachments/store.js +336 -26
  239. package/src/database/record/auditing.js +6 -3
  240. package/src/database/record/index.js +197 -42
  241. package/src/database/record/instance-relationships/belongs-to.js +2 -1
  242. package/src/database/record/validators/uniqueness.js +8 -5
  243. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  244. package/src/frontend-model-controller.js +136 -53
  245. package/src/frontend-model-resource/base-resource.js +87 -37
  246. package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
  247. package/src/frontend-models/base.js +307 -59
  248. package/src/frontend-models/preloader.js +6 -3
  249. package/src/frontend-models/query.js +15 -6
  250. package/src/frontend-models/resource-definition.js +21 -2
  251. package/src/frontend-models/use-destroyed-event.js +1 -1
  252. package/src/frontend-models/use-model-class-event.js +2 -2
  253. package/src/frontend-models/use-updated-event.js +1 -1
  254. package/src/frontend-models/websocket-channel.js +256 -93
  255. package/src/frontend-models/websocket-publishers.js +218 -18
  256. package/src/http-server/client/websocket-session.js +13 -1
  257. package/src/http-server/websocket-channel.js +15 -2
  258. package/src/sync/assets/cache.js +998 -0
  259. package/src/sync/assets/types.js +62 -0
  260. package/src/sync/sync-api-client.js +3 -2
  261. package/src/sync/sync-client.js +2 -1
  262. package/src/sync/sync-envelope-replay-service.js +5 -4
  263. package/src/sync/sync-publisher.js +3 -2
  264. package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  265. package/src/testing/test-profiler.js +2 -2
  266. package/src/testing/test-runner.js +496 -990
  267. package/src/testing/test.js +164 -306
  268. package/src/testing/velocious-attempt-executor.js +441 -0
  269. package/src/testing/velocious-runner-reporter.js +264 -0
  270. package/src/testing/velocious-suite-hook-executor.js +43 -0
  271. package/src/testing/velocious-test-arguments.js +64 -0
  272. package/src/utils/model-primary-key.js +171 -0
  273. package/src/utils/sha256-bytes-hex.js +132 -0
  274. package/src/utils/sha256-hex.js +7 -135
  275. package/build/src/testing/testing-package-adapter.d.ts +0 -9
  276. package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
  277. package/build/src/testing/testing-package-adapter.js +0 -69
  278. package/build/testing/testing-package-adapter.js +0 -76
  279. package/src/testing/testing-package-adapter.js +0 -76
@@ -1,26 +1,30 @@
1
1
  // @ts-check
2
2
 
3
- import {addTrackedStackToError} from "../utils/with-tracked-stack.js"
4
3
  import fs from "node:fs/promises"
5
4
  import path from "path"
6
- import {format} from "node:util"
7
5
  import {AsyncLocalStorage} from "node:async_hooks"
6
+ import {createTestContext, defaultTestContext} from "@velocious/testing"
7
+ import {TestRunner as PackageTestRunner} from "@velocious/testing/runner"
8
8
  import Application from "../../src/application.js"
9
- import BacktraceCleaner from "../utils/backtrace-cleaner-node.js"
10
- import {TestDatabaseAccessRevokedError} from "../environment-handlers/base.js"
11
9
  import RequestClient from "./request-client.js"
12
10
  import picocolors from "picocolors"
13
11
  import restArgsError from "../utils/rest-args-error.js"
14
- import {testConfig, testEvents, tests} from "./test.js"
15
- import {pathToFileURL} from "url"
16
- import {clearDeliveries} from "../mailer.js"
12
+ import {testConfig} from "./test.js"
13
+ import {fileURLToPath, pathToFileURL} from "url"
17
14
  import SharedTransactionBroker from "./shared-transaction-broker.js"
18
15
  import { SHARED_TRANSACTION_BROKER_ENV } from "./shared-transaction-proxy-driver.js"
19
- import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
16
+ import VelociousAttemptExecutor from "./velocious-attempt-executor.js"
17
+ import VelociousRunnerReporter, {AbortRemainingTestsError} from "./velocious-runner-reporter.js"
18
+ import VelociousSuiteHookExecutor from "./velocious-suite-hook-executor.js"
19
+ import VelociousTestArguments from "./velocious-test-arguments.js"
20
+
21
+ /** @typedef {typeof defaultTestContext} PackageTestContext */
22
+ /** @typedef {(typeof defaultTestContext.registry.suites)[number]} PackageSuiteDeclaration */
23
+ /** @typedef {PackageSuiteDeclaration["tests"][number]} PackageTestDeclaration */
24
+ /** @typedef {PackageSuiteDeclaration["hooks"]["beforeAll"][number]} PackageHookDeclaration */
25
+ /** @typedef {PackageSuiteDeclaration | PackageTestDeclaration | PackageHookDeclaration} PackageRegistration */
26
+ /** @typedef {{hadRetries: boolean, options: PackageTestDeclaration["options"], retries: number | undefined}} PackageRetryOptionRestoration */
20
27
 
21
- /**
22
- * ConsoleMethodName type.
23
- * @typedef {"log" | "info" | "warn" | "error" | "debug"} ConsoleMethodName */
24
28
  /**
25
29
  * AttemptConsoleOutput type.
26
30
  * @typedef {object} AttemptConsoleOutput
@@ -62,6 +66,7 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
62
66
  * @property {number} [line] - Source line number.
63
67
  * @property {string} [ownerFilePath] - Deterministic importing test file.
64
68
  * @property {(arg: TestArgs) => (void|Promise<void>)} function - Test callback to execute.
69
+ * @property {PackageTestDeclaration} [declaration] - Package declaration.
65
70
  */
66
71
  /**
67
72
  * FailedTestDetail type.
@@ -73,13 +78,6 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
73
78
  * @property {string} [consoleOutput] - Captured console output while test ran.
74
79
  * @property {string} [consoleLogPath] - Saved console log path.
75
80
  */
76
- /**
77
- * ActiveAfterAllScopeEntry type.
78
- * @typedef {object} ActiveAfterAllScopeEntry
79
- * @property {TestsArgument} tests - Scope test tree.
80
- * @property {boolean} afterAllsRun - Whether cleanup hooks have run.
81
- * @property {string} [profileScopeId] - Opaque profile scope identifier.
82
- */
83
81
  /**
84
82
  * Defines this typedef.
85
83
  * @typedef {(args: {configuration: import("../configuration.js").default, testArgs: TestArgs, testData: TestData}) => (void|Promise<void>)} AfterBeforeEachCallbackType
@@ -120,9 +118,8 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
120
118
  * @property {Record<string, TestsArgument>} subs - Optional child nodes. Each item is another `Node`, allowing recursion.
121
119
  */
122
120
  /**
123
- * Marks the error thrown by {@link runWithTimeout} so the caller can tell a
124
- * lifecycle timeout (the promise is still running detached) apart from an
125
- * ordinary test failure (the promise already settled).
121
+ * Marks the error thrown by the attempt timeout so the runner can distinguish
122
+ * detached lifecycle cleanup from an ordinary test failure.
126
123
  * @typedef {Error & {velociousTestTimeout?: true}} TestTimeoutError
127
124
  */
128
125
  /**
@@ -145,99 +142,6 @@ import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
145
142
  * @property {import("../database/pool/base.js").TestSharedConnectionRegistration | undefined} sharedRegistration - Physical-key shared registration once published.
146
143
  */
147
144
 
148
- /**
149
- * Runs run with timeout.
150
- *
151
- * On timeout the wrapped `promise` is NOT cancelled — it keeps running detached.
152
- * The rejected error is tagged with `velociousTestTimeout` so the runner knows
153
- * the lifecycle (and its afterEach database cleanup) is still in flight and can
154
- * wait for it to settle before the next test reuses the shared connection.
155
- * @param {Promise<ReturnType<typeof JSON.parse>> | ReturnType<typeof JSON.parse>} promise - Promise or value.
156
- * @param {number} timeoutMs - Timeout in milliseconds.
157
- * @param {string} testDescription - Test description.
158
- * @returns {Promise<ReturnType<typeof JSON.parse>>} - Resolves or rejects based on timeout or promise result.
159
- */
160
- function runWithTimeout(promise, timeoutMs, testDescription) {
161
- const timeoutSeconds = (timeoutMs / 1000).toFixed(3).replace(/\.?0+$/, "")
162
- /** @type {TestTimeoutError} */
163
- const timeoutError = new Error(`Timed out after ${timeoutSeconds}s: ${testDescription}`)
164
- timeoutError.velociousTestTimeout = true
165
-
166
- return new Promise((resolve, reject) => {
167
- const timeout = setTimeout(() => reject(timeoutError), timeoutMs)
168
-
169
- Promise.resolve(promise).then((result) => {
170
- clearTimeout(timeout)
171
- resolve(result)
172
- }).catch((error) => {
173
- clearTimeout(timeout)
174
- reject(error)
175
- })
176
- })
177
- }
178
-
179
- /**
180
- * Waits for an abandoned (timed-out) test lifecycle to settle, bounded by a
181
- * grace period, so its afterEach database cleanup runs on the shared connection
182
- * before the next test reuses it. Returns the fulfillment/rejection outcome if
183
- * the lifecycle settles, or a pending outcome once the grace elapses.
184
- *
185
- * The grace timer is kept ref'd so it cannot let Node exit with an unsettled
186
- * top-level await when the timed-out lifecycle has no ref'd handles of its own
187
- * (for example a stalled mocked async API). Once the caller continues past this
188
- * await, the timer has already resolved and no longer anchors the event loop.
189
- * @param {Promise<ReturnType<typeof JSON.parse>>} lifecycle - The abandoned per-test lifecycle promise.
190
- * @param {number} graceMs - Maximum time to wait for the lifecycle to settle.
191
- * @returns {Promise<{settled: false} | {settled: true, status: "fulfilled"} | {settled: true, status: "rejected", reason: unknown}>} - Settlement outcome.
192
- */
193
- function awaitSettledOrGrace(lifecycle, graceMs) {
194
- return new Promise((resolve) => {
195
- let settled = false
196
- const graceTimer = setTimeout(() => {
197
- if (settled) return
198
-
199
- settled = true
200
- resolve({settled: false})
201
- }, graceMs)
202
-
203
- Promise.resolve(lifecycle).then(
204
- () => {
205
- if (settled) return
206
-
207
- settled = true
208
- clearTimeout(graceTimer)
209
- resolve({settled: true, status: "fulfilled"})
210
- },
211
- (reason) => {
212
- if (settled) return
213
-
214
- settled = true
215
- clearTimeout(graceTimer)
216
- resolve({settled: true, status: "rejected", reason})
217
- }
218
- )
219
- })
220
- }
221
-
222
- /**
223
- * Checks whether a late lifecycle stopped only because its test access was revoked.
224
- * @param {unknown} error - Lifecycle rejection.
225
- * @returns {boolean} - Whether every contained error is expected revocation.
226
- */
227
- function isTestDatabaseAccessRevocation(error) {
228
- if (error instanceof TestDatabaseAccessRevokedError) return true
229
- if (error instanceof AggregateError) {
230
- return error.errors.length > 0 && error.errors.every((nestedError) => isTestDatabaseAccessRevocation(nestedError))
231
- }
232
-
233
- return false
234
- }
235
-
236
- /**
237
- * Captured console methods.
238
- * @type {ConsoleMethodName[]} */
239
- const CAPTURED_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"]
240
-
241
145
  /**
242
146
  * Runs to file slug.
243
147
  * @param {string} value - Value to sanitize.
@@ -252,10 +156,8 @@ function toFileSlug(value) {
252
156
  }
253
157
 
254
158
  export default class TestRunner {
255
- /**
256
- * Narrows the runtime value to the documented type.
257
- * @type {ActiveAfterAllScopeEntry[]} */
258
- _activeAfterAllScopes
159
+ /** @type {PackageTestContext} */
160
+ _context
259
161
 
260
162
  /**
261
163
  * Narrows the runtime value to the documented type.
@@ -266,6 +168,7 @@ export default class TestRunner {
266
168
  * Runs constructor.
267
169
  * @param {object} args - Options object.
268
170
  * @param {import("../configuration.js").default} args.configuration - Configuration instance.
171
+ * @param {PackageTestContext} [args.context] - Declaration context.
269
172
  * @param {string[] | string} [args.excludeTags] - Tags to exclude.
270
173
  * @param {string[] | string} [args.includeTags] - Tags to include.
271
174
  * @param {Array<string>} args.testFiles - Test files.
@@ -273,18 +176,17 @@ export default class TestRunner {
273
176
  * @param {RegExp[]} [args.examplePatterns] - Example patterns.
274
177
  * @param {import("./test-profiler.js").default} [args.profiler] - Opt-in profiler.
275
178
  */
276
- constructor({configuration, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
179
+ constructor({configuration, context = defaultTestContext, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
277
180
  restArgsError(restArgs)
278
181
 
279
182
  if (!configuration) throw new Error("configuration is required")
280
183
 
281
184
  this._configuration = configuration
185
+ this._context = context
282
186
  this._sharedTransactionCoordinatorOwnerStorage = new AsyncLocalStorage()
283
187
  this._testDatabaseAccessScopeStorage = new AsyncLocalStorage()
284
188
  this._excludeTags = this.normalizeTags(excludeTags)
285
- this._excludeTagSet = new Set(this._excludeTags)
286
189
  this._includeTags = this.normalizeTags(includeTags)
287
- this._includeTagSet = new Set(this._includeTags)
288
190
  this._testFiles = testFiles
289
191
  this._lineFilters = lineFilters || {}
290
192
  this._examplePatterns = examplePatterns || []
@@ -294,14 +196,49 @@ export default class TestRunner {
294
196
  this._failedTests = 0
295
197
  this._successfulTests = 0
296
198
  this._testsCount = 0
297
- this._activeAfterAllScopes = []
298
199
  this._failedTestDetails = []
299
200
  /** @type {{fullDescription: string, filePath: string, line: number} | null} */
300
201
  this._lastTestContext = null
301
202
  /** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
302
203
  this._testDurations = []
204
+ /** @type {WeakMap<PackageTestDeclaration, {testArgs: TestArgs, testData: TestData}>} */
205
+ this._testCompatibility = new WeakMap()
206
+ /** @type {WeakSet<PackageTestDeclaration>} */
207
+ this._injectedTests = new WeakSet()
208
+ /** @type {WeakSet<PackageTestDeclaration>} */
209
+ this._completedTests = new WeakSet()
210
+ /** @type {WeakMap<PackageTestDeclaration, {descriptions: string[], testDescription: string, fullDescription: string, ownerFilePath: string | undefined, suites: PackageSuiteDeclaration[]}>} */
211
+ this._testMetadata = new WeakMap()
212
+ /** @type {WeakMap<PackageHookDeclaration, {declarationIndex: number, declarationScopeId: string | undefined, ownerFilePath: string | undefined}>} */
213
+ this._hookMetadata = new WeakMap()
214
+ /** @type {WeakMap<PackageTestDeclaration, Map<number, {abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean}>>} */
215
+ this._attemptOutcomes = new WeakMap()
216
+ /** @type {Array<{suite: PackageSuiteDeclaration, phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} */
217
+ this._suiteHookFailures = []
218
+ /** @type {Map<string, PackageTestDeclaration[]>} */
219
+ this._testsByFullName = new Map()
220
+ /** @type {WeakMap<PackageRegistration, string>} */
221
+ this._declarationOwners = new WeakMap()
222
+ /** @type {PackageTestRunner | undefined} */
223
+ this._packageRunner = undefined
224
+ /** @type {import("@velocious/testing/runner").TestRunResult | undefined} */
225
+ this._packageResult = undefined
226
+ /** @type {Map<string, TestData> | undefined} */
227
+ this._legacyFixtureDataByFullName = undefined
228
+ /** @type {{filePath?: string, line?: number}} */
229
+ this._legacyFixtureLocation = {}
230
+ this._attemptExecutor = new VelociousAttemptExecutor({testRunner: this})
231
+ this._runnerReporter = new VelociousRunnerReporter({testRunner: this})
232
+ this._suiteHookExecutor = new VelociousSuiteHookExecutor({testRunner: this})
233
+ this._testArguments = new VelociousTestArguments({testRunner: this})
303
234
  }
304
235
 
236
+ /**
237
+ * Gets the package declaration context.
238
+ * @returns {PackageTestContext} - Package declaration context.
239
+ */
240
+ getTestContext() { return this._context }
241
+
305
242
  /**
306
243
  * Runs get configuration.
307
244
  * @returns {import("../configuration.js").default} - The configuration.
@@ -343,24 +280,6 @@ export default class TestRunner {
343
280
  return await this._profiler.runSpan(metadata, callback)
344
281
  }
345
282
 
346
- /**
347
- * Adds declaration metadata to hooks only for an active profile.
348
- * @template {AfterBeforeEachCallbackObjectType | BeforeAfterAllCallbackObjectType} T
349
- * @param {T[]} hooks - Hooks declared in one scope.
350
- * @param {string | undefined} declarationScopeId - Profile scope identifier.
351
- * @param {string | undefined} ownerFilePath - Scope owner file.
352
- * @returns {T[]} - Profile-aware hook entries.
353
- */
354
- profileHookEntries(hooks, declarationScopeId, ownerFilePath) {
355
- if (!this._profiler) return hooks
356
-
357
- return hooks.map((hook, declarationIndex) => Object.assign({}, hook, {
358
- declarationIndex: hook.declarationIndex ?? declarationIndex,
359
- declarationScopeId: hook.declarationScopeId ?? declarationScopeId,
360
- ownerFilePath: hook.ownerFilePath ?? ownerFilePath
361
- }))
362
- }
363
-
364
283
  /**
365
284
  * Runs normalize tags.
366
285
  * @param {string[] | string | undefined} tags - Tags.
@@ -666,107 +585,6 @@ export default class TestRunner {
666
585
  return new Set([...this._excludeTags, ...configTags])
667
586
  }
668
587
 
669
- /**
670
- * Runs has matching tag.
671
- * @param {string[] | string | undefined} testTags - Test tags.
672
- * @param {Set<string>} tagSet - Tag set.
673
- * @returns {boolean} - Whether any tags match.
674
- */
675
- hasMatchingTag(testTags, tagSet) {
676
- if (!tagSet.size) return false
677
-
678
- const normalized = this.normalizeTags(testTags)
679
-
680
- for (const tag of normalized) {
681
- if (tagSet.has(tag)) return true
682
- }
683
-
684
- return false
685
- }
686
-
687
- /**
688
- * Runs has runnable tests.
689
- * @param {TestsArgument} tests - Tests.
690
- * @param {string[]} [descriptions] - Description stack.
691
- * @param {boolean} [lineMatchedInScope] - Whether line matched in scope.
692
- * @returns {boolean} - Whether any tests in this scope will run.
693
- */
694
- hasRunnableTests(tests, descriptions = [], lineMatchedInScope = false) {
695
- for (const testDescription in tests.tests) {
696
- const testData = tests.tests[testDescription]
697
- const testArgs = /** @type {TestArgs} */ (Object.assign({}, testData.args))
698
- const includeByLine = lineMatchedInScope || this.matchesLineFilter(testData)
699
-
700
- if (this._onlyFocussed && !testArgs.focus) continue
701
- if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
702
-
703
- return true
704
- }
705
-
706
- for (const subDescription in tests.subs) {
707
- const subTest = tests.subs[subDescription]
708
- const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(subTest)
709
- const nextDescriptions = descriptions.concat([subDescription])
710
-
711
- if (this._onlyFocussed && !subTest.anyTestsFocussed) continue
712
- if (this.hasRunnableTests(subTest, nextDescriptions, scopeLineMatch)) return true
713
- }
714
-
715
- return false
716
- }
717
-
718
- /**
719
- * Runs should skip test.
720
- * @param {TestArgs} testArgs - Test args.
721
- * @param {TestData} testData - Test data.
722
- * @param {string} testDescription - Test description.
723
- * @param {string[]} descriptions - Description stack.
724
- * @param {boolean} lineMatchedInScope - Whether line matched in scope.
725
- * @returns {boolean} - Whether the test should be skipped.
726
- */
727
- shouldSkipTest(testArgs, testData, testDescription, descriptions, lineMatchedInScope) {
728
- if (this.hasTag(testArgs, "browser-only") && !this.isBrowserTestMode()) return true
729
- if (this.hasMatchingTag(testArgs.tags, this.getExcludeTagSet())) return true
730
-
731
- if (this._includeTagSet.size > 0 && !testArgs.focus) {
732
- if (!this.hasMatchingTag(testArgs.tags, this._includeTagSet)) return true
733
- }
734
-
735
- if (this.getExamplePatterns().length > 0) {
736
- const fullDescription = this.buildFullDescription(descriptions, testDescription)
737
- const matches = this.getExamplePatterns().some((pattern) => {
738
- pattern.lastIndex = 0
739
- return pattern.test(fullDescription)
740
- })
741
-
742
- if (!matches) return true
743
- }
744
-
745
- const lineFilters = this.getLineFilters()
746
-
747
- if (Object.keys(lineFilters).length > 0) {
748
- if (!lineMatchedInScope && !this.matchesLineFilter(testData)) return true
749
- }
750
-
751
- return false
752
- }
753
-
754
- /**
755
- * Runs matches line filter.
756
- * @param {TestData | TestsArgument} entry - Test entry.
757
- * @returns {boolean} - Whether line filter matches entry.
758
- */
759
- matchesLineFilter(entry) {
760
- if (!entry || !entry.filePath || !entry.line) return false
761
-
762
- const filePath = path.resolve(entry.filePath)
763
- const lines = this.getLineFilters()[filePath]
764
-
765
- if (!lines || lines.length === 0) return false
766
-
767
- return lines.includes(entry.line)
768
- }
769
-
770
588
  /**
771
589
  * Runs build full description.
772
590
  * @param {string[]} descriptions - Description stack.
@@ -1128,61 +946,48 @@ export default class TestRunner {
1128
946
 
1129
947
  if (!this._profiler) {
1130
948
  await environmentHandler.importTestFiles(this.getTestFiles())
1131
- synchronizeTestingPackageTests(tests)
1132
949
  return
1133
950
  }
1134
951
 
1135
952
  for (const testFile of this.getTestFiles()) {
1136
- const existingRegistrations = this.testRegistrationObjects(tests)
953
+ const existingRegistrations = this.testRegistrationObjects()
1137
954
 
1138
955
  await this._profiler.measurePhase("imports", async () => {
1139
956
  await environmentHandler.importTestFiles([testFile])
1140
957
  }, {filePath: testFile})
1141
- synchronizeTestingPackageTests(tests)
1142
- this.assignTestRegistrationOwnership(tests, existingRegistrations, testFile)
958
+ this.assignTestRegistrationOwnership(existingRegistrations, testFile)
1143
959
  }
1144
960
  }
1145
961
 
1146
962
  /**
1147
- * Collects registered scope, hook, and test objects by identity.
1148
- * @param {TestsArgument} scope - Test scope.
1149
- * @param {Set<object>} [registrations] - Accumulated identities.
1150
- * @returns {Set<object>} - Registration identities.
963
+ * Collects package declaration objects by identity.
964
+ * @param {Set<PackageRegistration>} [registrations] - Accumulated identities.
965
+ * @returns {Set<PackageRegistration>} - Registration identities.
1151
966
  */
1152
- testRegistrationObjects(scope, registrations = new Set()) {
1153
- registrations.add(scope)
1154
-
1155
- for (const hook of [...scope.beforeAlls, ...scope.beforeEaches, ...scope.afterEaches, ...scope.afterAlls]) {
1156
- registrations.add(hook)
967
+ testRegistrationObjects(registrations = new Set()) {
968
+ const visit = (/** @type {PackageSuiteDeclaration} */ suite) => {
969
+ registrations.add(suite)
970
+ for (const hook of [...suite.hooks.beforeAll, ...suite.hooks.beforeEach, ...suite.hooks.afterEach, ...suite.hooks.afterAll]) {
971
+ registrations.add(hook)
972
+ }
973
+ for (const testDeclaration of suite.tests) registrations.add(testDeclaration)
974
+ for (const childSuite of suite.suites) visit(childSuite)
1157
975
  }
1158
976
 
1159
- for (const testData of Object.values(scope.tests)) registrations.add(testData)
1160
- for (const childScope of Object.values(scope.subs)) this.testRegistrationObjects(childScope, registrations)
977
+ for (const suite of this.getTestContext().registry.suites) visit(suite)
1161
978
 
1162
979
  return registrations
1163
980
  }
1164
981
 
1165
982
  /**
1166
- * Assigns deterministic ownership to registrations added by one entry file,
1167
- * including declarations originating in a helper imported by that entry file.
1168
- * @param {TestsArgument} scope - Test scope.
1169
- * @param {Set<object>} previousRegistrations - Identities present before import.
983
+ * Assigns deterministic ownership to package declarations added by one entry file.
984
+ * @param {Set<PackageRegistration>} previousRegistrations - Identities present before import.
1170
985
  * @param {string} ownerFilePath - Importing entry file.
1171
986
  * @returns {void}
1172
987
  */
1173
- assignTestRegistrationOwnership(scope, previousRegistrations, ownerFilePath) {
1174
- if (!previousRegistrations.has(scope)) scope.ownerFilePath ??= ownerFilePath
1175
-
1176
- for (const hook of [...scope.beforeAlls, ...scope.beforeEaches, ...scope.afterEaches, ...scope.afterAlls]) {
1177
- if (!previousRegistrations.has(hook)) hook.ownerFilePath ??= ownerFilePath
1178
- }
1179
-
1180
- for (const testData of Object.values(scope.tests)) {
1181
- if (!previousRegistrations.has(testData)) testData.ownerFilePath ??= ownerFilePath
1182
- }
1183
-
1184
- for (const childScope of Object.values(scope.subs)) {
1185
- this.assignTestRegistrationOwnership(childScope, previousRegistrations, ownerFilePath)
988
+ assignTestRegistrationOwnership(previousRegistrations, ownerFilePath) {
989
+ for (const registration of this.testRegistrationObjects()) {
990
+ if (!previousRegistrations.has(registration)) this._declarationOwners.set(registration, ownerFilePath)
1186
991
  }
1187
992
  }
1188
993
 
@@ -1190,7 +995,7 @@ export default class TestRunner {
1190
995
  * Runs is failed.
1191
996
  * @returns {boolean} - Whether failed.
1192
997
  */
1193
- isFailed() { return this._failedTests !== undefined && this._failedTests > 0 }
998
+ isFailed() { return this._failedTests !== undefined && (this._failedTests > 0 || this._packageResult?.status === "failed") }
1194
999
 
1195
1000
  /**
1196
1001
  * Runs get failed tests.
@@ -1279,7 +1084,7 @@ export default class TestRunner {
1279
1084
  * @returns {number} - The executed tests count.
1280
1085
  */
1281
1086
  getExecutedTestsCount() {
1282
- return this._testDurations.length
1087
+ return this._packageResult?.tests.length ?? this._testDurations.length
1283
1088
  }
1284
1089
 
1285
1090
  /**
@@ -1305,17 +1110,79 @@ export default class TestRunner {
1305
1110
  this._abortRemainingTests = false
1306
1111
  this._failedTestDetails = []
1307
1112
  this._testDurations = []
1113
+ this._testCompatibility = new WeakMap()
1114
+ this._injectedTests = new WeakSet()
1115
+ this._completedTests = new WeakSet()
1116
+ this._testMetadata = new WeakMap()
1117
+ this._hookMetadata = new WeakMap()
1118
+ this._attemptOutcomes = new WeakMap()
1119
+ this._suiteHookFailures = []
1120
+ this._testsByFullName = new Map()
1121
+ this._packageResult = undefined
1122
+ const context = this.getTestContext()
1123
+ /** @type {string | undefined} */
1124
+ let ownerFilePath
1125
+
1126
+ context.reset({config: true})
1127
+ context.setDeclarationLocator(() => this.captureTestDeclarationLocation(ownerFilePath))
1308
1128
  const testingConfigPath = this.getConfiguration().getTesting()
1309
1129
 
1310
- if (testingConfigPath) {
1311
- await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
1312
- await this.getConfiguration().getEnvironmentHandler().importTestingConfigPath()
1313
- })
1130
+ await context.describe("", {databaseCleaning: {transaction: true}}, async () => {
1131
+ if (testingConfigPath) {
1132
+ await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
1133
+ await this.getConfiguration().getEnvironmentHandler().importTestingConfigPath()
1134
+ })
1135
+ }
1136
+
1137
+ if (!this._profiler) {
1138
+ await this.importTestFiles()
1139
+ } else {
1140
+ for (const testFile of this.getTestFiles()) {
1141
+ ownerFilePath = testFile
1142
+ const existingRegistrations = this.testRegistrationObjects()
1143
+
1144
+ await this._profiler.measurePhase("imports", async () => {
1145
+ await this.getConfiguration().getEnvironmentHandler().importTestFiles([testFile])
1146
+ }, {filePath: testFile})
1147
+ this.assignTestRegistrationOwnership(existingRegistrations, testFile)
1148
+ }
1149
+ }
1150
+ })
1151
+ ownerFilePath = undefined
1152
+ this.analyzeDeclarations()
1153
+ }
1154
+
1155
+ /**
1156
+ * Captures a test source location without attributing package/facade frames.
1157
+ * @param {string | undefined} ownerFilePath - Importing entry file fallback.
1158
+ * @returns {{filePath?: string, line?: number}} - Declaration location.
1159
+ */
1160
+ captureTestDeclarationLocation(ownerFilePath) {
1161
+ const stack = new Error().stack?.split("\n") || []
1162
+
1163
+ for (const stackLine of stack) {
1164
+ const match = stackLine.match(/(?:\(|\s)(file:\/\/.*?|\/[^"]*?):(\d+):(\d+)\)?$/u)
1165
+ if (!match) continue
1166
+
1167
+ let filePath = match[1]
1168
+ if (filePath.startsWith("file://")) {
1169
+ try {
1170
+ filePath = fileURLToPath(filePath)
1171
+ } catch {
1172
+ continue
1173
+ }
1174
+ }
1175
+ const resolvedFilePath = path.resolve(filePath)
1176
+ const portablePath = resolvedFilePath.replaceAll(path.sep, "/")
1177
+
1178
+ if (portablePath.endsWith("/src/testing/test-runner.js")) continue
1179
+ if (portablePath.endsWith("/src/testing/test.js")) continue
1180
+ if (portablePath.includes("/node_modules/@velocious/testing/")) continue
1181
+
1182
+ return {filePath: resolvedFilePath, line: Number(match[2])}
1314
1183
  }
1315
1184
 
1316
- await this.importTestFiles()
1317
- await this.analyzeTests(tests)
1318
- this._onlyFocussed = this.anyTestsFocussed
1185
+ return ownerFilePath ? {filePath: ownerFilePath} : {}
1319
1186
  }
1320
1187
 
1321
1188
  /**
@@ -1440,13 +1307,7 @@ export default class TestRunner {
1440
1307
  process.on("uncaughtException", onUncaughtException)
1441
1308
 
1442
1309
  try {
1443
- await this.runTests({
1444
- afterEaches: [],
1445
- beforeEaches: [],
1446
- tests,
1447
- descriptions: [],
1448
- indentLevel: 0
1449
- })
1310
+ await this.runPackageTests()
1450
1311
 
1451
1312
  // A rejection scheduled by the final test (a detached rejected promise,
1452
1313
  // or an afterCommit callback rejecting as the suite drains) is reported
@@ -1467,708 +1328,410 @@ export default class TestRunner {
1467
1328
  * @returns {Promise<void>} - Resolves when cleanup hooks finish.
1468
1329
  */
1469
1330
  async runAfterAllsForActiveScopes() {
1470
- const scopes = [...this._activeAfterAllScopes].reverse()
1471
- /** @type {unknown[]} */
1472
- const afterAllErrors = []
1331
+ const failureStart = this._suiteHookFailures.length
1473
1332
 
1474
- for (const scope of scopes) {
1475
- try {
1476
- await this.runAfterAllsForScope(scope)
1477
- } catch (error) {
1478
- afterAllErrors.push(error)
1333
+ await this._packageRunner?.cleanupActiveSuites()
1334
+ this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
1335
+ }
1336
+
1337
+ /** Builds declaration metadata used only by framework adapters and projections. */
1338
+ analyzeDeclarations() {
1339
+ const visit = (/** @type {PackageSuiteDeclaration} */ suite, /** @type {PackageSuiteDeclaration[]} */ ancestors, /** @type {string | undefined} */ parentProfileScopeId) => {
1340
+ const suites = [...ancestors, suite]
1341
+ const descriptions = suites.map((entry) => entry.name).filter((name) => name !== "")
1342
+ const ownerFilePath = this._declarationOwners.get(suite) ?? suite.location.filePath
1343
+ const profileScopeId = this._profiler?.scopeId(suite, {
1344
+ descriptions,
1345
+ filePath: ownerFilePath,
1346
+ line: suite.location.line,
1347
+ parentId: parentProfileScopeId
1348
+ })
1349
+
1350
+ for (const hooks of Object.values(suite.hooks)) {
1351
+ hooks.forEach((hook, declarationIndex) => {
1352
+ this._hookMetadata.set(hook, {
1353
+ declarationIndex,
1354
+ declarationScopeId: profileScopeId,
1355
+ ownerFilePath: this._declarationOwners.get(hook) ?? hook.location.filePath ?? ownerFilePath
1356
+ })
1357
+ })
1479
1358
  }
1480
- }
1481
1359
 
1482
- this._activeAfterAllScopes = []
1360
+ for (const testDeclaration of suite.tests) {
1361
+ const fullDescription = this.buildFullDescription(descriptions, testDeclaration.name)
1362
+ const declarations = this._testsByFullName.get(fullDescription) || []
1363
+
1364
+ declarations.push(testDeclaration)
1365
+ this._testsByFullName.set(fullDescription, declarations)
1366
+ this._testMetadata.set(testDeclaration, {
1367
+ descriptions,
1368
+ testDescription: testDeclaration.name,
1369
+ fullDescription,
1370
+ ownerFilePath: this._declarationOwners.get(testDeclaration) ?? testDeclaration.location.filePath ?? ownerFilePath,
1371
+ suites
1372
+ })
1373
+ const legacyTestData = this._legacyFixtureDataByFullName?.get(fullDescription)
1374
+ if (legacyTestData) {
1375
+ this._testCompatibility.set(testDeclaration, {
1376
+ testArgs: this._testArguments.copy(testDeclaration),
1377
+ testData: legacyTestData
1378
+ })
1379
+ }
1380
+ this._testsCount++
1381
+ if (testDeclaration.state === "run" && (testDeclaration.focus || suites.some((entry) => entry.focus))) {
1382
+ this.anyTestsFocussed = true
1383
+ }
1384
+ }
1483
1385
 
1484
- if (afterAllErrors.length == 1) throw afterAllErrors[0]
1485
- if (afterAllErrors.length > 1) {
1486
- throw new AggregateError(afterAllErrors, "Multiple active afterAll scopes failed", {cause: afterAllErrors[0]})
1386
+ for (const childSuite of suite.suites) visit(childSuite, suites, profileScopeId)
1487
1387
  }
1388
+
1389
+ for (const suite of this.getTestContext().registry.suites) visit(suite, [], undefined)
1488
1390
  }
1489
1391
 
1490
1392
  /**
1491
- * Runs analyze tests.
1492
- * @param {TestsArgument} tests - Tests.
1493
- * @returns {{anyTestsFocussed: boolean}} - Whether any tests in the tree are focused.
1393
+ * Gets package hook compatibility metadata.
1394
+ * @param {PackageHookDeclaration} hook - Package hook declaration.
1395
+ * @returns {{declarationIndex: number, declarationScopeId: string | undefined, ownerFilePath: string | undefined}} - Hook metadata.
1494
1396
  */
1495
- analyzeTests(tests) {
1496
- let anyTestsFocussedFound = false
1397
+ hookMetadata(hook) {
1398
+ return this._hookMetadata.get(hook) || {declarationIndex: 0, declarationScopeId: undefined, ownerFilePath: hook.location.filePath}
1399
+ }
1497
1400
 
1498
- for (const testDescription in tests.tests) {
1499
- const testData = tests.tests[testDescription]
1500
- const testArgs = Object.assign({}, testData.args)
1401
+ /**
1402
+ * Gets package test compatibility metadata.
1403
+ * @param {PackageTestDeclaration} test - Package test declaration.
1404
+ * @returns {{descriptions: string[], testDescription: string, fullDescription: string, ownerFilePath: string | undefined, suites: PackageSuiteDeclaration[]}} - Declaration metadata.
1405
+ */
1406
+ testMetadata(test) {
1407
+ const metadata = this._testMetadata.get(test)
1408
+ if (!metadata) throw new Error(`Missing package test metadata: ${test.name}`)
1409
+ return metadata
1410
+ }
1501
1411
 
1502
- this._testsCount++
1412
+ /**
1413
+ * Gets stable compatibility data for a package declaration.
1414
+ * @param {PackageTestDeclaration} test - Package test declaration.
1415
+ * @returns {{testArgs: TestArgs, testData: TestData}} - Stable compatibility data.
1416
+ */
1417
+ testData(test) {
1418
+ let compatibility = this._testCompatibility.get(test)
1503
1419
 
1504
- if (testArgs.focus) {
1505
- anyTestsFocussedFound = true
1506
- this.anyTestsFocussed = true
1420
+ if (!compatibility) {
1421
+ const testArgs = this._testArguments.copy(test)
1422
+ const metadata = this.testMetadata(test)
1423
+ const testData = {
1424
+ args: testArgs,
1425
+ declaration: test,
1426
+ filePath: test.location.filePath,
1427
+ function: test.callback,
1428
+ line: test.location.line,
1429
+ ownerFilePath: metadata.ownerFilePath
1507
1430
  }
1431
+ compatibility = {testArgs, testData}
1432
+ this._testCompatibility.set(test, compatibility)
1508
1433
  }
1509
1434
 
1510
- for (const subDescription in tests.subs) {
1511
- const subTest = tests.subs[subDescription]
1512
- const {anyTestsFocussed} = this.analyzeTests(subTest)
1435
+ return compatibility
1436
+ }
1513
1437
 
1514
- if (anyTestsFocussed) {
1515
- anyTestsFocussedFound = true
1516
- }
1438
+ /**
1439
+ * Injects framework collaborators into stable compatibility data once.
1440
+ * @param {PackageTestDeclaration} test - Package test declaration.
1441
+ * @returns {Promise<{testArgs: TestArgs, testData: TestData}>} - Injected compatibility data.
1442
+ */
1443
+ async testCompatibility(test) {
1444
+ const compatibility = this.testData(test)
1517
1445
 
1518
- subTest.anyTestsFocussed = anyTestsFocussed
1446
+ if (!this._injectedTests.has(test)) {
1447
+ await this._testArguments.inject(compatibility.testArgs)
1448
+ this._injectedTests.add(test)
1519
1449
  }
1520
1450
 
1521
- return {anyTestsFocussed: anyTestsFocussedFound}
1451
+ return compatibility
1522
1452
  }
1523
1453
 
1524
1454
  /**
1525
- * Runs every after-each hook while preserving the first failure.
1526
- * @param {object} args - Hook execution arguments.
1527
- * @param {AfterBeforeEachCallbackObjectType[]} args.afterEaches - Hooks to run.
1528
- * @param {TestArgs} args.testArgs - Current test arguments.
1529
- * @param {TestData} args.testData - Current test data.
1530
- * @returns {Promise<void>} - Resolves after every hook runs.
1455
+ * Records a raw framework attempt outcome.
1456
+ * @param {PackageTestDeclaration} test - Package test declaration.
1457
+ * @param {number} attemptNumber - One-based attempt number.
1458
+ * @param {{abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean}} outcome - Raw attempt outcome.
1459
+ * @returns {void}
1531
1460
  */
1532
- async runAfterEaches({afterEaches, testArgs, testData}) {
1533
- /** @type {unknown[]} */
1534
- const afterEachErrors = []
1461
+ recordAttemptOutcome(test, attemptNumber, outcome) {
1462
+ const outcomes = this._attemptOutcomes.get(test) || new Map()
1463
+ outcomes.set(attemptNumber, outcome)
1464
+ this._attemptOutcomes.set(test, outcomes)
1465
+ if (outcome.abortRemainingTests) this._abortRemainingTests = true
1466
+ }
1535
1467
 
1536
- for (const afterEachData of afterEaches) {
1537
- try {
1538
- await this.runProfileSpan({
1539
- phase: "afterEach",
1540
- declarationIndex: afterEachData.declarationIndex,
1541
- declarationScopeId: afterEachData.declarationScopeId,
1542
- filePath: afterEachData.ownerFilePath
1543
- }, async () => {
1544
- await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1545
- })
1546
- } catch (error) {
1547
- afterEachErrors.push(error)
1548
- }
1549
- }
1468
+ /**
1469
+ * Gets a raw framework attempt outcome.
1470
+ * @param {PackageTestDeclaration} test - Package test declaration.
1471
+ * @param {number} attemptNumber - One-based attempt number.
1472
+ * @returns {{abortRemainingTests: boolean, error: ReturnType<typeof JSON.parse>, failed: boolean} | undefined} - Raw attempt outcome.
1473
+ */
1474
+ attemptOutcome(test, attemptNumber) { return this._attemptOutcomes.get(test)?.get(attemptNumber) }
1550
1475
 
1551
- if (afterEachErrors.length == 1) throw afterEachErrors[0]
1552
- if (afterEachErrors.length > 1) {
1553
- throw new AggregateError(afterEachErrors, "Multiple afterEach hooks failed", {cause: afterEachErrors[0]})
1554
- }
1555
- }
1476
+ /**
1477
+ * Records a raw suite-hook failure.
1478
+ * @param {object} failure - Suite-hook failure.
1479
+ * @param {PackageSuiteDeclaration} failure.suite - Owning package suite.
1480
+ * @param {"beforeAll" | "afterAll"} failure.phase - Hook phase.
1481
+ * @param {ReturnType<typeof JSON.parse>} failure.error - Raw hook failure.
1482
+ * @returns {void}
1483
+ */
1484
+ recordSuiteHookFailure(failure) { this._suiteHookFailures.push(failure) }
1556
1485
 
1557
1486
  /**
1558
- * Runs run tests.
1559
- * @param {object} args - Options object.
1560
- * @param {Array<AfterBeforeEachCallbackObjectType>} args.afterEaches - After eaches.
1561
- * @param {Array<AfterBeforeEachCallbackObjectType>} args.beforeEaches - Before eaches.
1562
- * @param {TestsArgument} args.tests - Tests.
1563
- * @param {string[]} args.descriptions - Descriptions.
1564
- * @param {number} args.indentLevel - Indent level.
1565
- * @param {boolean} [args.lineMatchedInScope] - Whether line matched in scope.
1566
- * @param {string} [args.parentProfileScopeId] - Parent profile scope.
1567
- * @returns {Promise<void>} - Resolves when complete.
1487
+ * Gets the raw ancestor setup failure for a package test.
1488
+ * @param {PackageTestDeclaration} test - Package test declaration.
1489
+ * @returns {ReturnType<typeof JSON.parse>} - Raw setup failure.
1568
1490
  */
1569
- async runTests({afterEaches, beforeEaches, tests, descriptions, indentLevel, lineMatchedInScope = false, parentProfileScopeId}) {
1570
- const environmentHandler = this.getConfiguration().getEnvironmentHandler()
1491
+ setupFailureFor(test) {
1492
+ const suites = this.testMetadata(test).suites
1493
+ return this._suiteHookFailures.find((failure) => failure.phase === "beforeAll" && suites.includes(failure.suite))?.error
1494
+ }
1571
1495
 
1572
- environmentHandler.installSharedTransactionCoordinatorOwnerStorage(this._sharedTransactionCoordinatorOwnerStorage)
1573
- environmentHandler.installTestDatabaseAccessScopeStorage(this._testDatabaseAccessScopeStorage)
1574
- const leftPadding = " ".repeat(indentLevel * 2)
1575
- const scopeOwnerFilePath = tests.ownerFilePath ?? tests.filePath
1576
- const profileScopeId = this._profiler?.scopeId(tests, {
1577
- descriptions,
1578
- filePath: scopeOwnerFilePath,
1579
- line: tests.line,
1580
- parentId: parentProfileScopeId
1581
- })
1582
- const ownAfterEaches = [...this.profileHookEntries(tests.afterEaches, profileScopeId, scopeOwnerFilePath)].reverse()
1583
- const ownBeforeEaches = this.profileHookEntries(tests.beforeEaches, profileScopeId, scopeOwnerFilePath)
1584
- const newAfterEaches = [...ownAfterEaches, ...afterEaches]
1585
- const newBeforeEaches = [...beforeEaches, ...ownBeforeEaches]
1586
- const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(tests)
1587
- const shouldRunAnyTests = this.hasRunnableTests(tests, descriptions, scopeLineMatch)
1496
+ /**
1497
+ * Finds the next incomplete declaration with a package full name.
1498
+ * @param {string} fullName - Package full name.
1499
+ * @returns {PackageTestDeclaration | undefined} - Next matching declaration.
1500
+ */
1501
+ findTestDeclaration(fullName) {
1502
+ return this._testsByFullName.get(fullName)?.find((test) => !this._completedTests.has(test))
1503
+ }
1588
1504
 
1589
- if (!shouldRunAnyTests) return
1505
+ /**
1506
+ * Marks a package declaration complete.
1507
+ * @param {PackageTestDeclaration} test - Completed declaration.
1508
+ * @returns {void}
1509
+ */
1510
+ completeTestDeclaration(test) { this._completedTests.add(test) }
1590
1511
 
1591
- /** @type {ActiveAfterAllScopeEntry} */
1592
- const scopeEntry = {tests, afterAllsRun: false, profileScopeId}
1593
- this._activeAfterAllScopes.push(scopeEntry)
1594
- /** @type {unknown[]} */
1595
- const scopeErrors = []
1512
+ /**
1513
+ * Gets the effective package retry count.
1514
+ * @param {PackageTestDeclaration} test - Package test declaration.
1515
+ * @returns {number} - Effective retry count.
1516
+ */
1517
+ retryCount(test) {
1518
+ const value = test.options.retries ?? test.options.retry ?? this.getTestContext().config.retries
1519
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
1520
+ }
1596
1521
 
1597
- try {
1598
- const beforeAlls = this.profileHookEntries(tests.beforeAlls || [], profileScopeId, scopeOwnerFilePath)
1599
-
1600
- for (const beforeAllData of beforeAlls) {
1601
- await this.runProfileSpan({
1602
- phase: "beforeAll",
1603
- declarationIndex: beforeAllData.declarationIndex,
1604
- declarationScopeId: beforeAllData.declarationScopeId,
1605
- filePath: beforeAllData.ownerFilePath
1606
- }, async () => {
1607
- await beforeAllData.callback({configuration: this.getConfiguration()})
1522
+ /**
1523
+ * Normalizes retry inputs for the package execution boundary while retaining
1524
+ * the declarations' original public options after the run.
1525
+ * @returns {() => void} - Restores original declaration options.
1526
+ */
1527
+ normalizePackageRetriesForExecution() {
1528
+ /** @type {PackageRetryOptionRestoration[]} */
1529
+ const restorations = []
1530
+ /**
1531
+ * Normalizes declarations in one suite.
1532
+ * @param {PackageSuiteDeclaration} suite - Suite whose tests are normalized.
1533
+ */
1534
+ const visit = (suite) => {
1535
+ for (const test of suite.tests) {
1536
+ // Capture compatibility arguments before temporarily adapting package
1537
+ // execution options so callbacks retain their declared values/identity.
1538
+ this.testData(test)
1539
+ restorations.push({
1540
+ hadRetries: Object.hasOwn(test.options, "retries"),
1541
+ options: test.options,
1542
+ retries: test.options.retries
1608
1543
  })
1544
+ test.options.retries = this.retryCount(test)
1609
1545
  }
1610
1546
 
1611
- for (const testDescription in tests.tests) {
1612
- const testData = tests.tests[testDescription]
1613
- const testArgs = /** @type {TestArgs} */ (Object.assign({}, testData.args))
1614
- const includeByLine = scopeLineMatch || this.matchesLineFilter(testData)
1615
-
1616
- if (this._onlyFocussed && !testArgs.focus) continue
1617
- if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
1618
-
1619
- if (testArgs.type == "model" || testArgs.type == "request") {
1620
- testArgs.application = await this.application()
1621
- }
1622
-
1623
- if (testArgs.type == "request") {
1624
- testArgs.client = await this.requestClient()
1625
- }
1547
+ for (const childSuite of suite.suites) visit(childSuite)
1548
+ }
1626
1549
 
1627
- const retryCount = typeof testArgs.retry === "number" && Number.isFinite(testArgs.retry)
1628
- ? Math.max(0, Math.floor(testArgs.retry))
1629
- : 0
1630
- const configTimeoutSeconds = typeof testConfig.defaultTimeoutSeconds === "number" ? testConfig.defaultTimeoutSeconds : undefined
1631
- const timeoutSeconds = typeof testArgs.timeoutSeconds === "number" ? testArgs.timeoutSeconds : configTimeoutSeconds
1632
- const useTimeout = typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
1633
- const timeoutMs = useTimeout ? timeoutSeconds * 1000 : undefined
1634
- let retriesUsed = 0
1635
- let attemptNumber = 1
1636
- /**
1637
- * Attempt console outputs.
1638
- * @type {AttemptConsoleOutput[]} */
1639
- const attemptConsoleOutputs = []
1640
-
1641
- console.log(`${leftPadding}it ${testDescription}`)
1642
-
1643
- const testStartMs = Date.now()
1644
-
1645
- while (true) {
1646
- let shouldRetry = false
1647
- /**
1648
- * Defines caughtError.
1649
- * @type {ReturnType<typeof JSON.parse>} */
1650
- let caughtError
1651
- /**
1652
- * Defines failedError.
1653
- * @type {ReturnType<typeof JSON.parse>} */
1654
- let failedError
1655
- /**
1656
- * Defines lastError.
1657
- * @type {ReturnType<typeof JSON.parse>} */
1658
- let lastError
1659
- let willRetry = false
1660
- /**
1661
- * The per-test lifecycle promise, hoisted so the timeout branch can
1662
- * still wait for it to settle after runWithTimeout has abandoned it.
1663
- * @type {Promise<ReturnType<typeof JSON.parse>> | undefined} */
1664
- let testLifecycle
1665
- /** @type {{pool: import("../database/pool/base.js").default, registration: import("../database/pool/base.js").TestSharedConnectionRegistration}[]} */
1666
- let testSharedConnectionRegistrations = []
1667
- let testSharedConnectionsActive = false
1668
- /** @type {SharedTransactionBrokerRegistration | undefined} */
1669
- let sharedTransactionBrokerRegistration
1670
- /** @type {SharedTransactionBrokerRegistration | undefined} */
1671
- let sharedTransactionBrokerPreparation
1672
- /** @type {TransactionalTenantRegistration[]} */
1673
- const transactionalTenantRegistrations = []
1674
- /** @type {BrowserDummyConnectionRegistration[]} */
1675
- const browserDummyConnectionRegistrations = []
1676
- const testDatabaseAccessScope = {revoked: false}
1677
- /** @type {Set<Error>} */
1678
- const recordedTimeoutCleanupErrors = new Set()
1679
- testArgs.registerTransactionalTenant = async (args) => {
1680
- await this.registerTransactionalTenant(args, transactionalTenantRegistrations)
1681
- }
1682
- const stopConsoleCapture = this.startConsoleCapture({
1683
- passthrough: testConfig.consoleOutput === "live"
1684
- })
1685
- const profiler = this._profiler
1686
- const profileAttempt = profiler?.startAttempt({
1687
- descriptions,
1688
- attemptNumber,
1689
- testData,
1690
- testDescription
1691
- })
1692
- let attemptTimedOut = false
1550
+ for (const suite of this.getTestContext().registry.suites) visit(suite)
1693
1551
 
1694
- try {
1695
- // Run the whole per-test lifecycle (dummy/server startup, connection
1696
- // acquisition, beforeEach hooks, the test body and afterEach hooks) as
1697
- // one promise so the timeout below can cover all of it.
1698
- const runLifecycleCallback = async () => await this.runWithDummyIfNeeded(testArgs, async () => {
1699
- const useTransaction = testArgs.databaseCleaning?.transaction === true
1700
- const shouldTruncate = testArgs.databaseCleaning?.truncate ?? !useTransaction
1701
- const useSharedTestConnections = useTransaction || testArgs.type == "request"
1702
- const useTestConnections = useSharedTestConnections || shouldTruncate
1703
- const runTestAttempt = async () => {
1704
- // Register dynamic candidates before hooks so transaction state changes
1705
- // made during a hook are immediately visible to any in-process work.
1706
- // Prepare transaction sharing before hooks so long-lived services cannot
1707
- // use the shared connection while its coordinator is still missing.
1708
- if (useSharedTestConnections) {
1709
- testSharedConnectionRegistrations = this.activateTestSharedConnections()
1710
- testSharedConnectionsActive = true
1711
- }
1712
- /** @type {unknown[]} */
1713
- const lifecycleErrors = []
1714
- let runCleanupHooks = false
1715
-
1716
- try {
1717
- if (useSharedTestConnections) {
1718
- sharedTransactionBrokerPreparation = await this.prepareSharedTransactionBroker()
1719
- }
1720
- runCleanupHooks = true
1721
-
1722
- clearDeliveries()
1723
- for (const beforeEachData of newBeforeEaches) {
1724
- await this.runProfileSpan({
1725
- phase: "beforeEach",
1726
- declarationIndex: beforeEachData.declarationIndex,
1727
- declarationScopeId: beforeEachData.declarationScopeId,
1728
- filePath: beforeEachData.ownerFilePath
1729
- }, async () => {
1730
- await beforeEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1731
- })
1732
- }
1733
-
1734
- if (useSharedTestConnections) {
1735
- const activeSharedTransactionConnections = this.sharedTransactionConnections({transactionsOnly: true})
1736
- if (sharedTransactionBrokerPreparation && !this.sharedTransactionBrokerMatchesConnections(sharedTransactionBrokerPreparation, activeSharedTransactionConnections)) {
1737
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1738
- testSharedConnectionRegistrations = []
1739
- testSharedConnectionsActive = false
1740
- }
1741
-
1742
- sharedTransactionBrokerRegistration = await this.startSharedTransactionBroker(sharedTransactionBrokerPreparation, activeSharedTransactionConnections)
1743
- sharedTransactionBrokerPreparation = undefined
1744
- if (sharedTransactionBrokerRegistration && !testSharedConnectionsActive) {
1745
- testSharedConnectionRegistrations = this.activateTestSharedConnections()
1746
- testSharedConnectionsActive = true
1747
- }
1748
- }
1749
-
1750
- // Record which test is running so an async crash (an unhandled
1751
- // rejection detached from any await) that fires during or shortly
1752
- // after this test can be attributed to it in run()'s handler.
1753
- this._lastTestContext = {
1754
- fullDescription: this.buildFullDescription(descriptions, testDescription),
1755
- filePath: testData.filePath ?? "<unknown>",
1756
- line: testData.line ?? 0
1757
- }
1758
- await this.runProfileSpan({phase: "test body", filePath: testData.ownerFilePath ?? testData.filePath}, async () => {
1759
- await testData.function(testArgs)
1760
- })
1761
- } catch (error) {
1762
- lifecycleErrors.push(error)
1763
- }
1764
-
1765
- if (runCleanupHooks) {
1766
- try {
1767
- // Framework-owned post-commit broadcasts are intentionally
1768
- // detached; drain them before test cleanup so their DB
1769
- // checkouts cannot leak into the next test's lifecycle.
1770
- await this.getConfiguration().awaitPendingBroadcasts()
1771
- } catch (error) {
1772
- lifecycleErrors.push(error)
1773
- }
1774
-
1775
- try {
1776
- if (testSharedConnectionsActive) {
1777
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1778
- testSharedConnectionRegistrations = []
1779
- testSharedConnectionsActive = false
1780
- }
1781
- } catch (error) {
1782
- lifecycleErrors.push(error)
1783
- }
1784
-
1785
- try {
1786
- await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1787
- sharedTransactionBrokerRegistration = undefined
1788
- sharedTransactionBrokerPreparation = undefined
1789
- } catch (error) {
1790
- lifecycleErrors.push(error)
1791
- }
1792
-
1793
- try {
1794
- await this.runAfterEaches({afterEaches: newAfterEaches, testArgs, testData})
1795
- } catch (error) {
1796
- lifecycleErrors.push(error)
1797
- }
1798
-
1799
- try {
1800
- await this.cleanupTransactionalTenants(transactionalTenantRegistrations)
1801
- } catch (error) {
1802
- lifecycleErrors.push(error)
1803
- }
1804
- }
1805
-
1806
- if (testSharedConnectionsActive) {
1807
- try {
1808
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1809
- } catch (error) {
1810
- lifecycleErrors.push(error)
1811
- }
1812
- testSharedConnectionsActive = false
1813
- }
1814
-
1815
- if (lifecycleErrors.length == 1) throw lifecycleErrors[0]
1816
- if (lifecycleErrors.length > 1) {
1817
- throw new AggregateError(lifecycleErrors, "Test lifecycle and cleanup failed", {cause: lifecycleErrors[0]})
1818
- }
1819
- }
1820
-
1821
- if (useTestConnections) {
1822
- // Database cleaning requires one connection for beforeEach, the test
1823
- // body and afterEach; only transactions and requests share it dynamically.
1824
- await this.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, runTestAttempt)
1825
- } else {
1826
- await runTestAttempt()
1827
- }
1828
- }, browserDummyConnectionRegistrations)
1829
- const lifecycleCallback = async () => await this.getConfiguration().runWithTestDatabaseAccessScope(testDatabaseAccessScope, runLifecycleCallback)
1830
- testLifecycle = profileAttempt && profiler
1831
- ? profiler.runAttempt(profileAttempt, lifecycleCallback)
1832
- : lifecycleCallback()
1833
-
1834
- // Time out the ENTIRE lifecycle, not just the test body. A hang in any
1835
- // phase — a connection checkout that never resolves, a beforeEach/afterEach
1836
- // waiting on a lock, or dummy server startup — would otherwise stall the
1837
- // whole run indefinitely (until CI kills the build) instead of failing the
1838
- // single offending test.
1839
- if (useTimeout && timeoutMs !== undefined) {
1840
- await runWithTimeout(testLifecycle, timeoutMs, testDescription)
1841
- } else {
1842
- await testLifecycle
1843
- }
1844
-
1845
- // A test is successful only after its complete lifecycle settles.
1846
- // Cleanup failures and timed-out detached work must not overlap the
1847
- // final successful and failed counters used for executed-test totals.
1848
- this._successfulTests++
1849
- } catch (error) {
1850
- caughtError = error
1851
- lastError = error
1852
-
1853
- // A timeout REJECTS while the lifecycle keeps running detached on the
1854
- // shared per-suite connection — including its afterEach database
1855
- // cleanup (e.g. transaction rollback). If the next test starts before
1856
- // that rollback runs, its own startTransaction() implicitly COMMITS
1857
- // the timed-out test's rows on the shared connection, poisoning every
1858
- // later test in the shard (duplicate-key / foreign-key cascades from
1859
- // leaked fixtures). Wait — bounded — for the abandoned lifecycle to
1860
- // settle so its cleanup lands first. If it remains active after the
1861
- // bounded grace, quarantine its browser connections and stop running
1862
- // tests rather than sharing unsafe state.
1863
- const timedOut = Boolean(/** @type {TestTimeoutError} */ (error)?.velociousTestTimeout)
1864
- attemptTimedOut = timedOut
1865
-
1866
- if (timedOut && testLifecycle) {
1867
- const emergencyCleanupErrors = []
1868
-
1869
- if (profileAttempt && profiler) profiler.finishAttempt(profileAttempt, "timed-out")
1870
- const lifecycleOutcome = await awaitSettledOrGrace(testLifecycle, timeoutMs ?? 60000)
1871
-
1872
- if (lifecycleOutcome.settled && lifecycleOutcome.status === "rejected") {
1873
- emergencyCleanupErrors.push(lifecycleOutcome.reason)
1874
- }
1875
-
1876
- // If the abandoned lifecycle never settled within the grace, its
1877
- // cleanup has not completed. Quarantine browser-owned connections
1878
- // before any scope cleanup can race the abandoned callback.
1879
- if (!lifecycleOutcome.settled) {
1880
- testDatabaseAccessScope.revoked = true
1881
- void testLifecycle.catch((cleanupError) => {
1882
- if (isTestDatabaseAccessRevocation(cleanupError)) return
1883
- this.recordTimeoutCleanupFailure(cleanupError, "test lifecycle", recordedTimeoutCleanupErrors)
1884
- })
1885
- const quarantine = this.quarantineBrowserDummyConnections(browserDummyConnectionRegistrations)
1886
- const quarantineOutcome = await awaitSettledOrGrace(quarantine, timeoutMs ?? 60000)
1887
- const usesBrowserTransactions = testArgs.databaseCleaning?.transaction === true
1888
- const usesBrowserTruncation = testArgs.databaseCleaning?.truncate ?? !usesBrowserTransactions
1889
-
1890
- this._abortRemainingTests = this.isBrowserTestMode()
1891
- && this.hasTag(testArgs, "dummy")
1892
- && (usesBrowserTransactions || usesBrowserTruncation)
1893
-
1894
- if (quarantineOutcome.settled && quarantineOutcome.status === "rejected") {
1895
- emergencyCleanupErrors.push(quarantineOutcome.reason)
1896
- } else if (!quarantineOutcome.settled) {
1897
- void quarantine.catch((cleanupError) => {
1898
- this.recordTimeoutCleanupFailure(cleanupError, "browser dummy connection quarantine", recordedTimeoutCleanupErrors)
1899
- })
1900
- }
1901
- }
1902
-
1903
- try {
1904
- if (testSharedConnectionsActive) {
1905
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1906
- testSharedConnectionRegistrations = []
1907
- testSharedConnectionsActive = false
1908
- }
1909
- } catch (cleanupError) {
1910
- emergencyCleanupErrors.push(cleanupError)
1911
- }
1912
-
1913
- const brokerCleanup = this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1914
- const brokerCleanupOutcome = await awaitSettledOrGrace(brokerCleanup, timeoutMs ?? 60000)
1915
-
1916
- if (brokerCleanupOutcome.settled && brokerCleanupOutcome.status === "rejected") {
1917
- emergencyCleanupErrors.push(brokerCleanupOutcome.reason)
1918
- } else if (!brokerCleanupOutcome.settled) {
1919
- void brokerCleanup.catch((cleanupError) => {
1920
- this.recordTimeoutCleanupFailure(cleanupError, "shared transaction broker", recordedTimeoutCleanupErrors)
1921
- })
1922
- }
1923
- sharedTransactionBrokerRegistration = undefined
1924
- sharedTransactionBrokerPreparation = undefined
1925
- const emergencyCleanup = this.cleanupTransactionalTenants(transactionalTenantRegistrations, {discard: true})
1926
- const emergencyCleanupOutcome = await awaitSettledOrGrace(emergencyCleanup, timeoutMs ?? 60000)
1927
-
1928
- if (emergencyCleanupOutcome.settled && emergencyCleanupOutcome.status === "rejected") {
1929
- emergencyCleanupErrors.push(emergencyCleanupOutcome.reason)
1930
- } else if (!emergencyCleanupOutcome.settled) {
1931
- // The timed-out attempt must not block the runner indefinitely, but a
1932
- // later rollback/discard failure still becomes a visible test failure.
1933
- void emergencyCleanup.catch((cleanupError) => {
1934
- this.recordTimeoutCleanupFailure(cleanupError, "transactional tenant", recordedTimeoutCleanupErrors)
1935
- })
1936
- }
1937
-
1938
- if (emergencyCleanupErrors.length > 0) {
1939
- caughtError = new AggregateError(
1940
- [caughtError, ...emergencyCleanupErrors],
1941
- "Test timeout and emergency cleanup failed",
1942
- {cause: caughtError}
1943
- )
1944
- lastError = caughtError
1945
- }
1946
- }
1947
-
1948
- if (browserDummyConnectionRegistrations.some((registration) => registration.quarantined)) {
1949
- testDatabaseAccessScope.revoked = true
1950
- this._abortRemainingTests = true
1951
- }
1952
-
1953
- willRetry = !this._abortRemainingTests && retriesUsed < retryCount
1954
-
1955
- if (willRetry) {
1956
- retriesUsed++
1957
- }
1958
-
1959
- if (willRetry) {
1960
- shouldRetry = true
1961
- } else {
1962
- failedError = caughtError
1963
- }
1964
- } finally {
1965
- testDatabaseAccessScope.revoked = true
1966
- const consoleOutput = stopConsoleCapture()
1967
-
1968
- if (profileAttempt && profiler) {
1969
- profiler.finishAttempt(profileAttempt, caughtError === undefined
1970
- ? "passed"
1971
- : (attemptTimedOut ? "timed-out" : "failed"))
1972
- }
1973
-
1974
- if (consoleOutput) {
1975
- attemptConsoleOutputs.push({attemptNumber, output: consoleOutput})
1976
- }
1977
- }
1552
+ return () => {
1553
+ for (const restoration of restorations) {
1554
+ if (restoration.hadRetries) restoration.options.retries = restoration.retries
1555
+ else delete restoration.options.retries
1556
+ }
1557
+ }
1558
+ }
1978
1559
 
1979
- if (caughtError !== undefined) {
1980
- await this.emitEvent("testAttemptFailed", {
1981
- configuration: this.getConfiguration(),
1982
- descriptions,
1983
- error: caughtError,
1984
- attemptNumber,
1985
- nextAttempt: willRetry ? attemptNumber + 1 : undefined,
1986
- retriesUsed,
1987
- retryCount,
1988
- testArgs,
1989
- testData,
1990
- testDescription,
1991
- testRunner: this,
1992
- willRetry
1993
- })
1994
- }
1560
+ /**
1561
+ * Records one completed test duration.
1562
+ * @param {{durationMs: number, filePath: string, fullDescription: string, line: number}} duration - Completed test duration.
1563
+ * @returns {void}
1564
+ */
1565
+ recordTestDuration(duration) { this._testDurations.push(duration) }
1995
1566
 
1996
- if (shouldRetry) {
1997
- console.warn(picocolors.red(`${leftPadding} Retrying (${retriesUsed}/${retryCount}) after error: ${lastError instanceof Error ? lastError.message : String(lastError)}`))
1998
- await this.emitEvent("testRetrying", {
1999
- configuration: this.getConfiguration(),
2000
- descriptions,
2001
- error: lastError,
2002
- nextAttempt: attemptNumber + 1,
2003
- retriesUsed,
2004
- retryCount,
2005
- testArgs,
2006
- testData,
2007
- testDescription,
2008
- testRunner: this
2009
- })
2010
- }
1567
+ /** Records one successful package result. */
1568
+ recordSuccessfulTest() { this._successfulTests++ }
2011
1569
 
2012
- if (attemptNumber > 1) {
2013
- await this.emitEvent("testRetried", {
2014
- configuration: this.getConfiguration(),
2015
- descriptions,
2016
- error: lastError,
2017
- attemptNumber,
2018
- retriesUsed,
2019
- retryCount,
2020
- testArgs,
2021
- testData,
2022
- testDescription,
2023
- testRunner: this
2024
- })
2025
- }
2026
-
2027
- attemptNumber++
2028
-
2029
- if (shouldRetry) continue
2030
-
2031
- if (failedError) {
2032
- const consoleOutput = this.buildConsoleOutput(attemptConsoleOutputs)
2033
-
2034
- if (failedError instanceof Error) {
2035
- console.error(picocolors.red(`${leftPadding} Test failed: ${failedError.message}`))
2036
- addTrackedStackToError(failedError)
2037
-
2038
- const backtraceCleaner = new BacktraceCleaner(failedError)
2039
- const cleanedStack = backtraceCleaner.getCleanedStack()
2040
- const stackLines = cleanedStack?.split("\n")
2041
-
2042
- if (stackLines) {
2043
- for (const stackLine of stackLines) {
2044
- console.error(picocolors.red(`${leftPadding} ${stackLine}`))
2045
- }
2046
- }
2047
- } else {
2048
- console.error(picocolors.red(`${leftPadding} Test failed with a ${typeof failedError}: ${String(failedError)}`))
2049
- }
2050
-
2051
- this.printFailedConsoleOutput({consoleOutput, leftPadding})
2052
- this._failedTests++
2053
- this._failedTestDetails.push({
2054
- fullDescription: this.buildFullDescription(descriptions, testDescription),
2055
- filePath: testData.filePath,
2056
- line: testData.line,
2057
- error: failedError,
2058
- consoleOutput: consoleOutput || undefined
2059
- })
2060
-
2061
- await this.emitEvent("testFailed", {
2062
- configuration: this.getConfiguration(),
2063
- descriptions,
2064
- error: failedError,
2065
- testArgs,
2066
- testData,
2067
- testDescription,
2068
- testRunner: this
2069
- })
2070
-
2071
- this.printRerunCommand({descriptions, testDescription, testData, leftPadding})
2072
- }
1570
+ /**
1571
+ * Records one failed package test in the legacy result projection.
1572
+ * @param {object} args - Failed test metadata.
1573
+ * @param {string[]} args.descriptions - Parent descriptions.
1574
+ * @param {ReturnType<typeof JSON.parse>} args.error - Raw failure.
1575
+ * @param {string} args.consoleOutput - Captured console output.
1576
+ * @param {TestData} args.testData - Compatibility test data.
1577
+ * @param {string} args.testDescription - Test description.
1578
+ * @returns {void}
1579
+ */
1580
+ recordFailedTest({descriptions, error, consoleOutput, testData, testDescription}) {
1581
+ this._failedTests++
1582
+ this._failedTestDetails.push({
1583
+ fullDescription: this.buildFullDescription(descriptions, testDescription),
1584
+ filePath: testData.filePath,
1585
+ line: testData.line,
1586
+ error,
1587
+ consoleOutput: consoleOutput || undefined
1588
+ })
1589
+ }
2073
1590
 
2074
- break
2075
- }
1591
+ /**
1592
+ * Stores the completed package result.
1593
+ * @param {import("@velocious/testing/runner").TestRunResult} result - Package result.
1594
+ * @returns {void}
1595
+ */
1596
+ recordPackageResult(result) { this._packageResult = result }
2076
1597
 
2077
- this._testDurations.push({
2078
- fullDescription: this.buildFullDescription(descriptions, testDescription),
2079
- filePath: testData.filePath ?? "<unknown>",
2080
- line: testData.line ?? 0,
2081
- durationMs: Date.now() - testStartMs
2082
- })
1598
+ /**
1599
+ * Runs the package kernel with Velocious framework adapters.
1600
+ * @returns {Promise<void>} - Resolves after execution and teardown.
1601
+ */
1602
+ async runPackageTests() {
1603
+ const environmentHandler = this.getConfiguration().getEnvironmentHandler()
1604
+ environmentHandler.installSharedTransactionCoordinatorOwnerStorage(this._sharedTransactionCoordinatorOwnerStorage)
1605
+ environmentHandler.installTestDatabaseAccessScopeStorage(this._testDatabaseAccessScopeStorage)
1606
+ this._packageRunner = new PackageTestRunner({
1607
+ context: this.getTestContext(),
1608
+ includeTags: this._includeTags,
1609
+ excludeTags: [...this.getExcludeTagSet(), ...(this.isBrowserTestMode() ? [] : ["browser-only"])],
1610
+ examples: this.getExamplePatterns(),
1611
+ lineFilters: this.getLineFilters(),
1612
+ includeTagMode: "any",
1613
+ focusedTestsBypassIncludeTags: true,
1614
+ omitEmptySuiteNames: true,
1615
+ attemptExecutorOwnsTimeout: true,
1616
+ attemptExecutor: (input) => this._attemptExecutor.execute(input),
1617
+ testArgumentResolver: (input) => this._testArguments.resolve(input),
1618
+ suiteHookExecutor: (input) => this._suiteHookExecutor.execute(input),
1619
+ reporter: this._runnerReporter
1620
+ })
1621
+ const failureStart = this._suiteHookFailures.length
1622
+ const restoreRetryOptions = this.normalizePackageRetriesForExecution()
1623
+ let result
2083
1624
 
2084
- if (this._abortRemainingTests) break
2085
- }
1625
+ try {
1626
+ try {
1627
+ result = await this._packageRunner.run()
1628
+ } catch (error) {
1629
+ if (!(error instanceof AbortRemainingTestsError)) throw error
2086
1630
 
2087
- for (const subDescription in tests.subs) {
2088
- if (this._abortRemainingTests) break
2089
-
2090
- const subTest = tests.subs[subDescription]
2091
- const newDecriptions = descriptions.concat([subDescription])
2092
- const childScopeLineMatch = scopeLineMatch || this.matchesLineFilter(subTest)
2093
-
2094
- if (!this._onlyFocussed || subTest.anyTestsFocussed) {
2095
- console.log(`${leftPadding}${subDescription}`)
2096
- await this.runTests({
2097
- afterEaches: newAfterEaches,
2098
- beforeEaches: newBeforeEaches,
2099
- tests: subTest,
2100
- descriptions: newDecriptions,
2101
- indentLevel: indentLevel + 1,
2102
- lineMatchedInScope: childScopeLineMatch,
2103
- parentProfileScopeId: profileScopeId
2104
- })
2105
- }
1631
+ const afterAll = this.afterAllOutcome(this._suiteHookFailures.slice(failureStart))
1632
+ if (afterAll.failed) this.recordTimeoutCleanupFailure(afterAll.error, "afterAll")
1633
+ return
2106
1634
  }
2107
- } catch (error) {
2108
- scopeErrors.push(error)
2109
- }
2110
-
2111
- try {
2112
- await this.runAfterAllsForScope(scopeEntry)
2113
- } catch (error) {
2114
- scopeErrors.push(error)
2115
- }
2116
- const scopeIndex = this._activeAfterAllScopes.indexOf(scopeEntry)
2117
1635
 
2118
- if (scopeIndex >= 0) {
2119
- this._activeAfterAllScopes.splice(scopeIndex, 1)
1636
+ this.recordPackageResult(result)
1637
+ this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
1638
+ } finally {
1639
+ restoreRetryOptions()
2120
1640
  }
1641
+ }
2121
1642
 
2122
- if (scopeErrors.length > 0 && this._abortRemainingTests) {
2123
- const error = scopeErrors.length == 1
2124
- ? scopeErrors[0]
2125
- : new AggregateError(scopeErrors, "Test scope and afterAll cleanup failed", {cause: scopeErrors[0]})
1643
+ /**
1644
+ * Aggregates raw after-all failures without using error truthiness.
1645
+ * @param {Array<{phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} failures - Hook failures.
1646
+ * @returns {{failed: false} | {failed: true, error: ReturnType<typeof JSON.parse>}} - Explicit afterAll outcome.
1647
+ */
1648
+ afterAllOutcome(failures) {
1649
+ const afterAllErrors = failures.filter((failure) => failure.phase === "afterAll").map((failure) => failure.error)
2126
1650
 
2127
- this.recordTimeoutCleanupFailure(error, "afterAll")
2128
- return
1651
+ if (afterAllErrors.length === 0) return {failed: false}
1652
+ if (afterAllErrors.length === 1) return {failed: true, error: afterAllErrors[0]}
1653
+ return {
1654
+ failed: true,
1655
+ error: new AggregateError(afterAllErrors, "Multiple active afterAll scopes failed", {cause: afterAllErrors[0]})
2129
1656
  }
2130
- if (scopeErrors.length == 1) throw scopeErrors[0]
2131
- if (scopeErrors.length > 1) throw new AggregateError(scopeErrors, "Test scope and afterAll cleanup failed", {cause: scopeErrors[0]})
2132
1657
  }
2133
1658
 
2134
1659
  /**
2135
- * Runs run after alls for scope.
2136
- * @param {ActiveAfterAllScopeEntry} scopeEntry - Scope entry.
2137
- * @returns {Promise<void>} - Resolves when scope cleanup finishes.
1660
+ * Throws one raw or aggregated after-all failure.
1661
+ * @param {Array<{phase: "beforeAll" | "afterAll", error: ReturnType<typeof JSON.parse>}>} failures - Hook failures.
1662
+ * @returns {void}
2138
1663
  */
2139
- async runAfterAllsForScope(scopeEntry) {
2140
- if (scopeEntry.afterAllsRun) return
1664
+ throwAfterAllFailures(failures) {
1665
+ const afterAll = this.afterAllOutcome(failures)
2141
1666
 
2142
- scopeEntry.afterAllsRun = true
1667
+ if (afterAll.failed) throw afterAll.error
1668
+ }
2143
1669
 
2144
- const scopeOwnerFilePath = scopeEntry.tests.ownerFilePath ?? scopeEntry.tests.filePath
2145
- const afterAlls = [...this.profileHookEntries(
2146
- scopeEntry.tests.afterAlls || [],
2147
- scopeEntry.profileScopeId,
2148
- scopeOwnerFilePath
2149
- )].reverse()
2150
- /** @type {unknown[]} */
2151
- const afterAllErrors = []
1670
+ /**
1671
+ * Compatibility helper for focused framework lifecycle specs. It converts an
1672
+ * explicit legacy fixture into isolated package declarations; the package
1673
+ * runner remains the sole execution engine.
1674
+ * @param {object} args - Legacy fixture arguments.
1675
+ * @param {TestsArgument} args.tests - Fixture tree.
1676
+ * @returns {Promise<void>} - Resolves after package execution.
1677
+ */
1678
+ async runTests({tests}) {
1679
+ const context = createTestContext()
1680
+ const originalContext = this._context
1681
+ context.configureTests({
1682
+ consoleOutput: originalContext.config.consoleOutput,
1683
+ defaultTimeoutMs: originalContext.config.defaultTimeoutMs,
1684
+ excludeTags: originalContext.config.excludeTags,
1685
+ failedConsoleOutputMaxLines: originalContext.config.failedConsoleOutputMaxLines,
1686
+ retries: originalContext.config.retries
1687
+ })
1688
+ this._context = context
1689
+ this._testsCount = 0
1690
+ this._testCompatibility = new WeakMap()
1691
+ this._injectedTests = new WeakSet()
1692
+ this._completedTests = new WeakSet()
1693
+ this._testMetadata = new WeakMap()
1694
+ this._hookMetadata = new WeakMap()
1695
+ this._attemptOutcomes = new WeakMap()
1696
+ this._suiteHookFailures = []
1697
+ this._testsByFullName = new Map()
1698
+ this._legacyFixtureDataByFullName = new Map()
1699
+ context.setDeclarationLocator(() => this._legacyFixtureLocation)
1700
+ this.declareLegacyFixture(context, "", tests, [])
1701
+ this.analyzeDeclarations()
2152
1702
 
2153
- for (const afterAllData of afterAlls) {
2154
- try {
2155
- await this.runProfileSpan({
2156
- phase: "afterAll",
2157
- declarationIndex: afterAllData.declarationIndex,
2158
- declarationScopeId: afterAllData.declarationScopeId,
2159
- filePath: afterAllData.ownerFilePath
2160
- }, async () => {
2161
- await afterAllData.callback({configuration: this.getConfiguration()})
2162
- })
2163
- } catch (error) {
2164
- afterAllErrors.push(error)
2165
- }
1703
+ try {
1704
+ await this.runPackageTests()
1705
+ } finally {
1706
+ this._context = originalContext
2166
1707
  }
1708
+ }
2167
1709
 
2168
- if (afterAllErrors.length == 1) throw afterAllErrors[0]
2169
- if (afterAllErrors.length > 1) {
2170
- throw new AggregateError(afterAllErrors, "Multiple afterAll hooks failed", {cause: afterAllErrors[0]})
2171
- }
1710
+ /**
1711
+ * Declares an isolated legacy-shaped test fixture into a package context.
1712
+ * @param {PackageTestContext} context - Isolated package context.
1713
+ * @param {string} name - Suite name.
1714
+ * @param {TestsArgument} scope - Legacy fixture scope.
1715
+ * @param {string[]} descriptions - Ancestor descriptions.
1716
+ * @returns {void}
1717
+ */
1718
+ declareLegacyFixture(context, name, scope, descriptions) {
1719
+ this._legacyFixtureLocation = {filePath: scope.filePath, line: scope.line}
1720
+ context.describe(name, scope.args || {}, () => {
1721
+ for (const hook of scope.beforeAlls || []) context.beforeAll(hook.callback)
1722
+ for (const hook of scope.beforeEaches || []) context.beforeEach(hook.callback)
1723
+ for (const hook of scope.afterEaches || []) context.afterEach(hook.callback)
1724
+ for (const hook of scope.afterAlls || []) context.afterAll(hook.callback)
1725
+ const nextDescriptions = name === "" ? descriptions : [...descriptions, name]
1726
+ for (const [testName, testData] of Object.entries(scope.tests || {})) {
1727
+ this._legacyFixtureLocation = {filePath: testData.filePath, line: testData.line}
1728
+ this._legacyFixtureDataByFullName?.set(this.buildFullDescription(nextDescriptions, testName), testData)
1729
+ context.it(testName, testData.args, testData.function)
1730
+ }
1731
+ for (const [suiteName, childScope] of Object.entries(scope.subs || {})) {
1732
+ this.declareLegacyFixture(context, suiteName, childScope, nextDescriptions)
1733
+ }
1734
+ })
2172
1735
  }
2173
1736
 
2174
1737
  /**
@@ -2178,11 +1741,7 @@ export default class TestRunner {
2178
1741
  * @returns {Promise<void>} - Resolves when all listeners complete.
2179
1742
  */
2180
1743
  async emitEvent(eventName, payload) {
2181
- const listeners = testEvents.listeners(eventName)
2182
-
2183
- for (const listener of listeners) {
2184
- await listener(payload)
2185
- }
1744
+ await this._runnerReporter.emitEvent(eventName, payload)
2186
1745
  }
2187
1746
 
2188
1747
  /**
@@ -2298,57 +1857,4 @@ export default class TestRunner {
2298
1857
  }
2299
1858
  }
2300
1859
 
2301
- /**
2302
- * Runs start console capture.
2303
- * @param {object} [args] - Options object.
2304
- * @param {boolean} [args.passthrough] - Whether to pass through to the original console.
2305
- * @returns {() => string} - Stops the capture and returns captured text.
2306
- */
2307
- startConsoleCapture({passthrough = false} = {}) {
2308
- /**
2309
- * Lines.
2310
- * @type {string[]} */
2311
- const lines = []
2312
- /**
2313
- * Console object.
2314
- * @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
2315
- const consoleObject = /** @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */ (console)
2316
- /**
2317
- * Original console methods captured as direct references so stopping restores
2318
- * the exact method that was installed at capture start.
2319
- * @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
2320
- const originalConsoleMethods = {
2321
- debug: consoleObject.debug,
2322
- error: consoleObject.error,
2323
- info: consoleObject.info,
2324
- log: consoleObject.log,
2325
- warn: consoleObject.warn
2326
- }
2327
- let stopped = false
2328
- let outputText = ""
2329
-
2330
- for (const methodName of CAPTURED_CONSOLE_METHODS) {
2331
- consoleObject[methodName] = (...args) => {
2332
- lines.push(`[${new Date().toISOString()}] [${methodName}] ${format(...args)}`)
2333
-
2334
- if (passthrough) {
2335
- originalConsoleMethods[methodName].apply(consoleObject, args)
2336
- }
2337
- }
2338
- }
2339
-
2340
- return () => {
2341
- if (!stopped) {
2342
- stopped = true
2343
-
2344
- for (const methodName of CAPTURED_CONSOLE_METHODS) {
2345
- consoleObject[methodName] = originalConsoleMethods[methodName]
2346
- }
2347
-
2348
- outputText = lines.join("\n")
2349
- }
2350
-
2351
- return outputText
2352
- }
2353
- }
2354
1860
  }