velocious 1.0.644 → 1.0.646

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 (259) hide show
  1. package/README.md +11 -9
  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/sync-api-client.d.ts.map +1 -1
  152. package/build/src/sync/sync-api-client.js +4 -3
  153. package/build/src/sync/sync-client.d.ts.map +1 -1
  154. package/build/src/sync/sync-client.js +3 -2
  155. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  156. package/build/src/sync/sync-envelope-replay-service.js +6 -5
  157. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  158. package/build/src/sync/sync-publisher.js +4 -3
  159. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
  160. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
  161. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
  162. package/build/src/testing/test-profiler.d.ts +4 -4
  163. package/build/src/testing/test-profiler.d.ts.map +1 -1
  164. package/build/src/testing/test-profiler.js +3 -3
  165. package/build/src/testing/test-runner.d.ts +271 -117
  166. package/build/src/testing/test-runner.d.ts.map +1 -1
  167. package/build/src/testing/test-runner.js +496 -427
  168. package/build/src/testing/test.d.ts +11 -72
  169. package/build/src/testing/test.d.ts.map +1 -1
  170. package/build/src/testing/test.js +131 -284
  171. package/build/src/testing/velocious-attempt-executor.d.ts +14 -29
  172. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  173. package/build/src/testing/velocious-attempt-executor.js +52 -46
  174. package/build/src/testing/velocious-runner-reporter.d.ts +29 -34
  175. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -1
  176. package/build/src/testing/velocious-runner-reporter.js +145 -57
  177. package/build/src/testing/velocious-suite-hook-executor.d.ts +5 -23
  178. package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -1
  179. package/build/src/testing/velocious-suite-hook-executor.js +22 -47
  180. package/build/src/testing/velocious-test-arguments.d.ts +11 -6
  181. package/build/src/testing/velocious-test-arguments.d.ts.map +1 -1
  182. package/build/src/testing/velocious-test-arguments.js +18 -10
  183. package/build/src/utils/model-primary-key.d.ts +73 -0
  184. package/build/src/utils/model-primary-key.d.ts.map +1 -0
  185. package/build/src/utils/model-primary-key.js +144 -0
  186. package/build/sync/sync-api-client.js +3 -2
  187. package/build/sync/sync-client.js +2 -1
  188. package/build/sync/sync-envelope-replay-service.js +5 -4
  189. package/build/sync/sync-publisher.js +3 -2
  190. package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  191. package/build/testing/test-profiler.js +2 -2
  192. package/build/testing/test-runner.js +493 -449
  193. package/build/testing/test.js +164 -306
  194. package/build/testing/velocious-attempt-executor.js +55 -45
  195. package/build/testing/velocious-runner-reporter.js +156 -58
  196. package/build/testing/velocious-suite-hook-executor.js +22 -50
  197. package/build/testing/velocious-test-arguments.js +19 -10
  198. package/build/utils/model-primary-key.js +171 -0
  199. package/package.json +6 -5
  200. package/src/authorization/ability.js +36 -13
  201. package/src/background-jobs/adapter.js +9 -0
  202. package/src/background-jobs/main.js +35 -10
  203. package/src/background-jobs/store.js +217 -38
  204. package/src/background-jobs/types.js +15 -0
  205. package/src/configuration-types.js +1 -1
  206. package/src/configuration.js +45 -9
  207. package/src/database/drivers/base-column.js +8 -0
  208. package/src/database/drivers/mssql/sql/update.js +1 -3
  209. package/src/database/drivers/mysql/sql/update.js +1 -3
  210. package/src/database/drivers/pgsql/column.js +22 -0
  211. package/src/database/drivers/pgsql/sql/update.js +1 -3
  212. package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
  213. package/src/database/drivers/sqlite/sql/update.js +1 -3
  214. package/src/database/query/index.js +2 -2
  215. package/src/database/query/model-class-query.js +26 -10
  216. package/src/database/query/preloader/has-many.js +4 -1
  217. package/src/database/query/query-data.js +2 -1
  218. package/src/database/query/update-base.js +14 -0
  219. package/src/database/query/with-count.js +3 -1
  220. package/src/database/record/acts-as-list.js +37 -13
  221. package/src/database/record/attachments/store.js +336 -26
  222. package/src/database/record/auditing.js +6 -3
  223. package/src/database/record/index.js +197 -42
  224. package/src/database/record/instance-relationships/belongs-to.js +2 -1
  225. package/src/database/record/validators/uniqueness.js +8 -5
  226. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  227. package/src/frontend-model-controller.js +136 -53
  228. package/src/frontend-model-resource/base-resource.js +87 -37
  229. package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
  230. package/src/frontend-models/base.js +307 -59
  231. package/src/frontend-models/preloader.js +6 -3
  232. package/src/frontend-models/query.js +15 -6
  233. package/src/frontend-models/resource-definition.js +21 -2
  234. package/src/frontend-models/use-destroyed-event.js +1 -1
  235. package/src/frontend-models/use-model-class-event.js +2 -2
  236. package/src/frontend-models/use-updated-event.js +1 -1
  237. package/src/frontend-models/websocket-channel.js +256 -93
  238. package/src/frontend-models/websocket-publishers.js +218 -18
  239. package/src/http-server/client/websocket-session.js +13 -1
  240. package/src/http-server/websocket-channel.js +15 -2
  241. package/src/sync/sync-api-client.js +3 -2
  242. package/src/sync/sync-client.js +2 -1
  243. package/src/sync/sync-envelope-replay-service.js +5 -4
  244. package/src/sync/sync-publisher.js +3 -2
  245. package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  246. package/src/testing/test-profiler.js +2 -2
  247. package/src/testing/test-runner.js +493 -449
  248. package/src/testing/test.js +164 -306
  249. package/src/testing/velocious-attempt-executor.js +55 -45
  250. package/src/testing/velocious-runner-reporter.js +156 -58
  251. package/src/testing/velocious-suite-hook-executor.js +22 -50
  252. package/src/testing/velocious-test-arguments.js +19 -10
  253. package/src/utils/model-primary-key.js +171 -0
  254. package/build/src/testing/testing-package-adapter.d.ts +0 -9
  255. package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
  256. package/build/src/testing/testing-package-adapter.js +0 -69
  257. package/build/testing/testing-package-adapter.js +0 -76
  258. package/build/tsconfig.tsbuildinfo +0 -1
  259. package/src/testing/testing-package-adapter.js +0 -76
@@ -2,25 +2,29 @@
2
2
 
3
3
  import fs from "node:fs/promises"
4
4
  import path from "path"
5
- import {format} from "node:util"
6
5
  import {AsyncLocalStorage} from "node:async_hooks"
6
+ import {createTestContext, defaultTestContext} from "@velocious/testing"
7
+ import {TestRunner as PackageTestRunner} from "@velocious/testing/runner"
7
8
  import Application from "../../src/application.js"
8
9
  import RequestClient from "./request-client.js"
9
10
  import picocolors from "picocolors"
10
11
  import restArgsError from "../utils/rest-args-error.js"
11
- import {testConfig, tests} from "./test.js"
12
- import {pathToFileURL} from "url"
12
+ import {testConfig} from "./test.js"
13
+ import {fileURLToPath, pathToFileURL} from "url"
13
14
  import SharedTransactionBroker from "./shared-transaction-broker.js"
14
15
  import { SHARED_TRANSACTION_BROKER_ENV } from "./shared-transaction-proxy-driver.js"
15
- import {synchronizeTestingPackageTests} from "./testing-package-adapter.js"
16
16
  import VelociousAttemptExecutor from "./velocious-attempt-executor.js"
17
- import VelociousRunnerReporter from "./velocious-runner-reporter.js"
17
+ import VelociousRunnerReporter, {AbortRemainingTestsError} from "./velocious-runner-reporter.js"
18
18
  import VelociousSuiteHookExecutor from "./velocious-suite-hook-executor.js"
19
19
  import VelociousTestArguments from "./velocious-test-arguments.js"
20
20
 
21
- /**
22
- * ConsoleMethodName type.
23
- * @typedef {"log" | "info" | "warn" | "error" | "debug"} ConsoleMethodName */
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 */
27
+
24
28
  /**
25
29
  * AttemptConsoleOutput type.
26
30
  * @typedef {object} AttemptConsoleOutput
@@ -62,6 +66,7 @@ import VelociousTestArguments from "./velocious-test-arguments.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 VelociousTestArguments from "./velocious-test-arguments.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
@@ -144,11 +142,6 @@ import VelociousTestArguments from "./velocious-test-arguments.js"
144
142
  * @property {import("../database/pool/base.js").TestSharedConnectionRegistration | undefined} sharedRegistration - Physical-key shared registration once published.
145
143
  */
146
144
 
147
- /**
148
- * Captured console methods.
149
- * @type {ConsoleMethodName[]} */
150
- const CAPTURED_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"]
151
-
152
145
  /**
153
146
  * Runs to file slug.
154
147
  * @param {string} value - Value to sanitize.
@@ -163,10 +156,8 @@ function toFileSlug(value) {
163
156
  }
164
157
 
165
158
  export default class TestRunner {
166
- /**
167
- * Narrows the runtime value to the documented type.
168
- * @type {ActiveAfterAllScopeEntry[]} */
169
- _activeAfterAllScopes
159
+ /** @type {PackageTestContext} */
160
+ _context
170
161
 
171
162
  /**
172
163
  * Narrows the runtime value to the documented type.
@@ -177,6 +168,7 @@ export default class TestRunner {
177
168
  * Runs constructor.
178
169
  * @param {object} args - Options object.
179
170
  * @param {import("../configuration.js").default} args.configuration - Configuration instance.
171
+ * @param {PackageTestContext} [args.context] - Declaration context.
180
172
  * @param {string[] | string} [args.excludeTags] - Tags to exclude.
181
173
  * @param {string[] | string} [args.includeTags] - Tags to include.
182
174
  * @param {Array<string>} args.testFiles - Test files.
@@ -184,18 +176,17 @@ export default class TestRunner {
184
176
  * @param {RegExp[]} [args.examplePatterns] - Example patterns.
185
177
  * @param {import("./test-profiler.js").default} [args.profiler] - Opt-in profiler.
186
178
  */
187
- constructor({configuration, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
179
+ constructor({configuration, context = defaultTestContext, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
188
180
  restArgsError(restArgs)
189
181
 
190
182
  if (!configuration) throw new Error("configuration is required")
191
183
 
192
184
  this._configuration = configuration
185
+ this._context = context
193
186
  this._sharedTransactionCoordinatorOwnerStorage = new AsyncLocalStorage()
194
187
  this._testDatabaseAccessScopeStorage = new AsyncLocalStorage()
195
188
  this._excludeTags = this.normalizeTags(excludeTags)
196
- this._excludeTagSet = new Set(this._excludeTags)
197
189
  this._includeTags = this.normalizeTags(includeTags)
198
- this._includeTagSet = new Set(this._includeTags)
199
190
  this._testFiles = testFiles
200
191
  this._lineFilters = lineFilters || {}
201
192
  this._examplePatterns = examplePatterns || []
@@ -205,18 +196,49 @@ export default class TestRunner {
205
196
  this._failedTests = 0
206
197
  this._successfulTests = 0
207
198
  this._testsCount = 0
208
- this._activeAfterAllScopes = []
209
199
  this._failedTestDetails = []
210
200
  /** @type {{fullDescription: string, filePath: string, line: number} | null} */
211
201
  this._lastTestContext = null
212
202
  /** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
213
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 = {}
214
230
  this._attemptExecutor = new VelociousAttemptExecutor({testRunner: this})
215
231
  this._runnerReporter = new VelociousRunnerReporter({testRunner: this})
216
232
  this._suiteHookExecutor = new VelociousSuiteHookExecutor({testRunner: this})
217
233
  this._testArguments = new VelociousTestArguments({testRunner: this})
218
234
  }
219
235
 
236
+ /**
237
+ * Gets the package declaration context.
238
+ * @returns {PackageTestContext} - Package declaration context.
239
+ */
240
+ getTestContext() { return this._context }
241
+
220
242
  /**
221
243
  * Runs get configuration.
222
244
  * @returns {import("../configuration.js").default} - The configuration.
@@ -258,24 +280,6 @@ export default class TestRunner {
258
280
  return await this._profiler.runSpan(metadata, callback)
259
281
  }
260
282
 
261
- /**
262
- * Adds declaration metadata to hooks only for an active profile.
263
- * @template {AfterBeforeEachCallbackObjectType | BeforeAfterAllCallbackObjectType} T
264
- * @param {T[]} hooks - Hooks declared in one scope.
265
- * @param {string | undefined} declarationScopeId - Profile scope identifier.
266
- * @param {string | undefined} ownerFilePath - Scope owner file.
267
- * @returns {T[]} - Profile-aware hook entries.
268
- */
269
- profileHookEntries(hooks, declarationScopeId, ownerFilePath) {
270
- if (!this._profiler) return hooks
271
-
272
- return hooks.map((hook, declarationIndex) => Object.assign({}, hook, {
273
- declarationIndex: hook.declarationIndex ?? declarationIndex,
274
- declarationScopeId: hook.declarationScopeId ?? declarationScopeId,
275
- ownerFilePath: hook.ownerFilePath ?? ownerFilePath
276
- }))
277
- }
278
-
279
283
  /**
280
284
  * Runs normalize tags.
281
285
  * @param {string[] | string | undefined} tags - Tags.
@@ -581,107 +585,6 @@ export default class TestRunner {
581
585
  return new Set([...this._excludeTags, ...configTags])
582
586
  }
583
587
 
584
- /**
585
- * Runs has matching tag.
586
- * @param {string[] | string | undefined} testTags - Test tags.
587
- * @param {Set<string>} tagSet - Tag set.
588
- * @returns {boolean} - Whether any tags match.
589
- */
590
- hasMatchingTag(testTags, tagSet) {
591
- if (!tagSet.size) return false
592
-
593
- const normalized = this.normalizeTags(testTags)
594
-
595
- for (const tag of normalized) {
596
- if (tagSet.has(tag)) return true
597
- }
598
-
599
- return false
600
- }
601
-
602
- /**
603
- * Runs has runnable tests.
604
- * @param {TestsArgument} tests - Tests.
605
- * @param {string[]} [descriptions] - Description stack.
606
- * @param {boolean} [lineMatchedInScope] - Whether line matched in scope.
607
- * @returns {boolean} - Whether any tests in this scope will run.
608
- */
609
- hasRunnableTests(tests, descriptions = [], lineMatchedInScope = false) {
610
- for (const testDescription in tests.tests) {
611
- const testData = tests.tests[testDescription]
612
- const testArgs = /** @type {TestArgs} */ (Object.assign({}, testData.args))
613
- const includeByLine = lineMatchedInScope || this.matchesLineFilter(testData)
614
-
615
- if (this._onlyFocussed && !testArgs.focus) continue
616
- if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
617
-
618
- return true
619
- }
620
-
621
- for (const subDescription in tests.subs) {
622
- const subTest = tests.subs[subDescription]
623
- const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(subTest)
624
- const nextDescriptions = descriptions.concat([subDescription])
625
-
626
- if (this._onlyFocussed && !subTest.anyTestsFocussed) continue
627
- if (this.hasRunnableTests(subTest, nextDescriptions, scopeLineMatch)) return true
628
- }
629
-
630
- return false
631
- }
632
-
633
- /**
634
- * Runs should skip test.
635
- * @param {TestArgs} testArgs - Test args.
636
- * @param {TestData} testData - Test data.
637
- * @param {string} testDescription - Test description.
638
- * @param {string[]} descriptions - Description stack.
639
- * @param {boolean} lineMatchedInScope - Whether line matched in scope.
640
- * @returns {boolean} - Whether the test should be skipped.
641
- */
642
- shouldSkipTest(testArgs, testData, testDescription, descriptions, lineMatchedInScope) {
643
- if (this.hasTag(testArgs, "browser-only") && !this.isBrowserTestMode()) return true
644
- if (this.hasMatchingTag(testArgs.tags, this.getExcludeTagSet())) return true
645
-
646
- if (this._includeTagSet.size > 0 && !testArgs.focus) {
647
- if (!this.hasMatchingTag(testArgs.tags, this._includeTagSet)) return true
648
- }
649
-
650
- if (this.getExamplePatterns().length > 0) {
651
- const fullDescription = this.buildFullDescription(descriptions, testDescription)
652
- const matches = this.getExamplePatterns().some((pattern) => {
653
- pattern.lastIndex = 0
654
- return pattern.test(fullDescription)
655
- })
656
-
657
- if (!matches) return true
658
- }
659
-
660
- const lineFilters = this.getLineFilters()
661
-
662
- if (Object.keys(lineFilters).length > 0) {
663
- if (!lineMatchedInScope && !this.matchesLineFilter(testData)) return true
664
- }
665
-
666
- return false
667
- }
668
-
669
- /**
670
- * Runs matches line filter.
671
- * @param {TestData | TestsArgument} entry - Test entry.
672
- * @returns {boolean} - Whether line filter matches entry.
673
- */
674
- matchesLineFilter(entry) {
675
- if (!entry || !entry.filePath || !entry.line) return false
676
-
677
- const filePath = path.resolve(entry.filePath)
678
- const lines = this.getLineFilters()[filePath]
679
-
680
- if (!lines || lines.length === 0) return false
681
-
682
- return lines.includes(entry.line)
683
- }
684
-
685
588
  /**
686
589
  * Runs build full description.
687
590
  * @param {string[]} descriptions - Description stack.
@@ -1043,61 +946,48 @@ export default class TestRunner {
1043
946
 
1044
947
  if (!this._profiler) {
1045
948
  await environmentHandler.importTestFiles(this.getTestFiles())
1046
- synchronizeTestingPackageTests(tests)
1047
949
  return
1048
950
  }
1049
951
 
1050
952
  for (const testFile of this.getTestFiles()) {
1051
- const existingRegistrations = this.testRegistrationObjects(tests)
953
+ const existingRegistrations = this.testRegistrationObjects()
1052
954
 
1053
955
  await this._profiler.measurePhase("imports", async () => {
1054
956
  await environmentHandler.importTestFiles([testFile])
1055
957
  }, {filePath: testFile})
1056
- synchronizeTestingPackageTests(tests)
1057
- this.assignTestRegistrationOwnership(tests, existingRegistrations, testFile)
958
+ this.assignTestRegistrationOwnership(existingRegistrations, testFile)
1058
959
  }
1059
960
  }
1060
961
 
1061
962
  /**
1062
- * Collects registered scope, hook, and test objects by identity.
1063
- * @param {TestsArgument} scope - Test scope.
1064
- * @param {Set<object>} [registrations] - Accumulated identities.
1065
- * @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.
1066
966
  */
1067
- testRegistrationObjects(scope, registrations = new Set()) {
1068
- registrations.add(scope)
1069
-
1070
- for (const hook of [...scope.beforeAlls, ...scope.beforeEaches, ...scope.afterEaches, ...scope.afterAlls]) {
1071
- 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)
1072
975
  }
1073
976
 
1074
- for (const testData of Object.values(scope.tests)) registrations.add(testData)
1075
- for (const childScope of Object.values(scope.subs)) this.testRegistrationObjects(childScope, registrations)
977
+ for (const suite of this.getTestContext().registry.suites) visit(suite)
1076
978
 
1077
979
  return registrations
1078
980
  }
1079
981
 
1080
982
  /**
1081
- * Assigns deterministic ownership to registrations added by one entry file,
1082
- * including declarations originating in a helper imported by that entry file.
1083
- * @param {TestsArgument} scope - Test scope.
1084
- * @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.
1085
985
  * @param {string} ownerFilePath - Importing entry file.
1086
986
  * @returns {void}
1087
987
  */
1088
- assignTestRegistrationOwnership(scope, previousRegistrations, ownerFilePath) {
1089
- if (!previousRegistrations.has(scope)) scope.ownerFilePath ??= ownerFilePath
1090
-
1091
- for (const hook of [...scope.beforeAlls, ...scope.beforeEaches, ...scope.afterEaches, ...scope.afterAlls]) {
1092
- if (!previousRegistrations.has(hook)) hook.ownerFilePath ??= ownerFilePath
1093
- }
1094
-
1095
- for (const testData of Object.values(scope.tests)) {
1096
- if (!previousRegistrations.has(testData)) testData.ownerFilePath ??= ownerFilePath
1097
- }
1098
-
1099
- for (const childScope of Object.values(scope.subs)) {
1100
- 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)
1101
991
  }
1102
992
  }
1103
993
 
@@ -1105,7 +995,7 @@ export default class TestRunner {
1105
995
  * Runs is failed.
1106
996
  * @returns {boolean} - Whether failed.
1107
997
  */
1108
- isFailed() { return this._failedTests !== undefined && this._failedTests > 0 }
998
+ isFailed() { return this._failedTests !== undefined && (this._failedTests > 0 || this._packageResult?.status === "failed") }
1109
999
 
1110
1000
  /**
1111
1001
  * Runs get failed tests.
@@ -1194,7 +1084,7 @@ export default class TestRunner {
1194
1084
  * @returns {number} - The executed tests count.
1195
1085
  */
1196
1086
  getExecutedTestsCount() {
1197
- return this._testDurations.length
1087
+ return this._packageResult?.tests.length ?? this._testDurations.length
1198
1088
  }
1199
1089
 
1200
1090
  /**
@@ -1220,17 +1110,79 @@ export default class TestRunner {
1220
1110
  this._abortRemainingTests = false
1221
1111
  this._failedTestDetails = []
1222
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))
1223
1128
  const testingConfigPath = this.getConfiguration().getTesting()
1224
1129
 
1225
- if (testingConfigPath) {
1226
- await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
1227
- await this.getConfiguration().getEnvironmentHandler().importTestingConfigPath()
1228
- })
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])}
1229
1183
  }
1230
1184
 
1231
- await this.importTestFiles()
1232
- await this.analyzeTests(tests)
1233
- this._onlyFocussed = this.anyTestsFocussed
1185
+ return ownerFilePath ? {filePath: ownerFilePath} : {}
1234
1186
  }
1235
1187
 
1236
1188
  /**
@@ -1355,13 +1307,7 @@ export default class TestRunner {
1355
1307
  process.on("uncaughtException", onUncaughtException)
1356
1308
 
1357
1309
  try {
1358
- await this.runTests({
1359
- afterEaches: [],
1360
- beforeEaches: [],
1361
- tests,
1362
- descriptions: [],
1363
- indentLevel: 0
1364
- })
1310
+ await this.runPackageTests()
1365
1311
 
1366
1312
  // A rejection scheduled by the final test (a detached rejected promise,
1367
1313
  // or an afterCommit callback rejecting as the suite drains) is reported
@@ -1382,259 +1328,410 @@ export default class TestRunner {
1382
1328
  * @returns {Promise<void>} - Resolves when cleanup hooks finish.
1383
1329
  */
1384
1330
  async runAfterAllsForActiveScopes() {
1385
- const scopes = [...this._activeAfterAllScopes].reverse()
1386
- /** @type {unknown[]} */
1387
- const afterAllErrors = []
1331
+ const failureStart = this._suiteHookFailures.length
1388
1332
 
1389
- for (const scope of scopes) {
1390
- try {
1391
- await this.runAfterAllsForScope(scope)
1392
- } catch (error) {
1393
- 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
+ })
1394
1358
  }
1395
- }
1396
1359
 
1397
- 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
+ }
1398
1385
 
1399
- if (afterAllErrors.length == 1) throw afterAllErrors[0]
1400
- if (afterAllErrors.length > 1) {
1401
- throw new AggregateError(afterAllErrors, "Multiple active afterAll scopes failed", {cause: afterAllErrors[0]})
1386
+ for (const childSuite of suite.suites) visit(childSuite, suites, profileScopeId)
1402
1387
  }
1388
+
1389
+ for (const suite of this.getTestContext().registry.suites) visit(suite, [], undefined)
1403
1390
  }
1404
1391
 
1405
1392
  /**
1406
- * Runs analyze tests.
1407
- * @param {TestsArgument} tests - Tests.
1408
- * @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.
1409
1396
  */
1410
- analyzeTests(tests) {
1411
- let anyTestsFocussedFound = false
1397
+ hookMetadata(hook) {
1398
+ return this._hookMetadata.get(hook) || {declarationIndex: 0, declarationScopeId: undefined, ownerFilePath: hook.location.filePath}
1399
+ }
1412
1400
 
1413
- for (const testDescription in tests.tests) {
1414
- const testData = tests.tests[testDescription]
1415
- 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
+ }
1416
1411
 
1417
- 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)
1418
1419
 
1419
- if (testArgs.focus) {
1420
- anyTestsFocussedFound = true
1421
- 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
1422
1430
  }
1431
+ compatibility = {testArgs, testData}
1432
+ this._testCompatibility.set(test, compatibility)
1423
1433
  }
1424
1434
 
1425
- for (const subDescription in tests.subs) {
1426
- const subTest = tests.subs[subDescription]
1427
- const {anyTestsFocussed} = this.analyzeTests(subTest)
1435
+ return compatibility
1436
+ }
1428
1437
 
1429
- if (anyTestsFocussed) {
1430
- anyTestsFocussedFound = true
1431
- }
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)
1432
1445
 
1433
- subTest.anyTestsFocussed = anyTestsFocussed
1446
+ if (!this._injectedTests.has(test)) {
1447
+ await this._testArguments.inject(compatibility.testArgs)
1448
+ this._injectedTests.add(test)
1434
1449
  }
1435
1450
 
1436
- return {anyTestsFocussed: anyTestsFocussedFound}
1451
+ return compatibility
1437
1452
  }
1438
1453
 
1439
1454
  /**
1440
- * Runs every after-each hook while preserving the first failure.
1441
- * @param {object} args - Hook execution arguments.
1442
- * @param {AfterBeforeEachCallbackObjectType[]} args.afterEaches - Hooks to run.
1443
- * @param {TestArgs} args.testArgs - Current test arguments.
1444
- * @param {TestData} args.testData - Current test data.
1445
- * @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}
1446
1460
  */
1447
- async runAfterEaches({afterEaches, testArgs, testData}) {
1448
- await this._attemptExecutor.runAfterEaches({afterEaches, testArgs, testData})
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
1449
1466
  }
1450
1467
 
1451
1468
  /**
1452
- * Runs run tests.
1453
- * @param {object} args - Options object.
1454
- * @param {Array<AfterBeforeEachCallbackObjectType>} args.afterEaches - After eaches.
1455
- * @param {Array<AfterBeforeEachCallbackObjectType>} args.beforeEaches - Before eaches.
1456
- * @param {TestsArgument} args.tests - Tests.
1457
- * @param {string[]} args.descriptions - Descriptions.
1458
- * @param {number} args.indentLevel - Indent level.
1459
- * @param {boolean} [args.lineMatchedInScope] - Whether line matched in scope.
1460
- * @param {string} [args.parentProfileScopeId] - Parent profile scope.
1461
- * @returns {Promise<void>} - Resolves when complete.
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.
1462
1473
  */
1463
- async runTests({afterEaches, beforeEaches, tests, descriptions, indentLevel, lineMatchedInScope = false, parentProfileScopeId}) {
1464
- const environmentHandler = this.getConfiguration().getEnvironmentHandler()
1465
-
1466
- environmentHandler.installSharedTransactionCoordinatorOwnerStorage(this._sharedTransactionCoordinatorOwnerStorage)
1467
- environmentHandler.installTestDatabaseAccessScopeStorage(this._testDatabaseAccessScopeStorage)
1468
- const leftPadding = " ".repeat(indentLevel * 2)
1469
- const scopeOwnerFilePath = tests.ownerFilePath ?? tests.filePath
1470
- const profileScopeId = this._profiler?.scopeId(tests, {
1471
- descriptions,
1472
- filePath: scopeOwnerFilePath,
1473
- line: tests.line,
1474
- parentId: parentProfileScopeId
1475
- })
1476
- const ownAfterEaches = [...this.profileHookEntries(tests.afterEaches, profileScopeId, scopeOwnerFilePath)].reverse()
1477
- const ownBeforeEaches = this.profileHookEntries(tests.beforeEaches, profileScopeId, scopeOwnerFilePath)
1478
- const newAfterEaches = [...ownAfterEaches, ...afterEaches]
1479
- const newBeforeEaches = [...beforeEaches, ...ownBeforeEaches]
1480
- const scopeLineMatch = lineMatchedInScope || this.matchesLineFilter(tests)
1481
- const shouldRunAnyTests = this.hasRunnableTests(tests, descriptions, scopeLineMatch)
1474
+ attemptOutcome(test, attemptNumber) { return this._attemptOutcomes.get(test)?.get(attemptNumber) }
1482
1475
 
1483
- if (!shouldRunAnyTests) return
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) }
1484
1485
 
1485
- /** @type {ActiveAfterAllScopeEntry} */
1486
- const scopeEntry = {tests, afterAllsRun: false, profileScopeId}
1487
- this._activeAfterAllScopes.push(scopeEntry)
1488
- /** @type {unknown[]} */
1489
- const scopeErrors = []
1486
+ /**
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.
1490
+ */
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
+ }
1490
1495
 
1491
- try {
1492
- const beforeAlls = this.profileHookEntries(tests.beforeAlls || [], profileScopeId, scopeOwnerFilePath)
1493
-
1494
- await this._suiteHookExecutor.runBeforeAlls({hooks: beforeAlls})
1495
-
1496
- for (const testDescription in tests.tests) {
1497
- const testData = tests.tests[testDescription]
1498
- const testArgs = this._testArguments.copy(testData)
1499
- const includeByLine = scopeLineMatch || this.matchesLineFilter(testData)
1500
-
1501
- if (this._onlyFocussed && !testArgs.focus) continue
1502
- if (this.shouldSkipTest(testArgs, testData, testDescription, descriptions, includeByLine)) continue
1503
-
1504
- await this._testArguments.inject(testArgs)
1505
-
1506
- const retryCount = typeof testArgs.retry === "number" && Number.isFinite(testArgs.retry)
1507
- ? Math.max(0, Math.floor(testArgs.retry))
1508
- : 0
1509
- const configTimeoutSeconds = typeof testConfig.defaultTimeoutSeconds === "number" ? testConfig.defaultTimeoutSeconds : undefined
1510
- const timeoutSeconds = typeof testArgs.timeoutSeconds === "number" ? testArgs.timeoutSeconds : configTimeoutSeconds
1511
- const useTimeout = typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
1512
- const timeoutMs = useTimeout ? timeoutSeconds * 1000 : undefined
1513
- let retriesUsed = 0
1514
- let attemptNumber = 1
1515
- /**
1516
- * Attempt console outputs.
1517
- * @type {AttemptConsoleOutput[]} */
1518
- const attemptConsoleOutputs = []
1519
-
1520
- console.log(`${leftPadding}it ${testDescription}`)
1521
-
1522
- const testStartMs = Date.now()
1523
-
1524
- while (true) {
1525
- const attemptResult = await this._attemptExecutor.execute({
1526
- afterEaches: newAfterEaches,
1527
- attemptNumber,
1528
- beforeEaches: newBeforeEaches,
1529
- descriptions,
1530
- testArgs,
1531
- testData,
1532
- testDescription,
1533
- timeoutMs
1534
- })
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
+ }
1535
1504
 
1536
- if (attemptResult.consoleOutput) {
1537
- attemptConsoleOutputs.push({attemptNumber, output: attemptResult.consoleOutput})
1538
- }
1539
- if (attemptResult.abortRemainingTests) this._abortRemainingTests = true
1540
-
1541
- const willRetry = attemptResult.failed && !this._abortRemainingTests && retriesUsed < retryCount
1542
- if (willRetry) retriesUsed++
1543
-
1544
- await this._runnerReporter.reportAttempt({
1545
- attemptConsoleOutputs,
1546
- attemptNumber,
1547
- descriptions,
1548
- error: attemptResult.error,
1549
- failed: attemptResult.failed,
1550
- leftPadding,
1551
- retriesUsed,
1552
- retryCount,
1553
- testArgs,
1554
- testData,
1555
- testDescription,
1556
- willRetry
1557
- })
1558
- attemptNumber++
1505
+ /**
1506
+ * Marks a package declaration complete.
1507
+ * @param {PackageTestDeclaration} test - Completed declaration.
1508
+ * @returns {void}
1509
+ */
1510
+ completeTestDeclaration(test) { this._completedTests.add(test) }
1559
1511
 
1560
- if (!willRetry) break
1561
- }
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
+ }
1562
1521
 
1563
- this._testDurations.push({
1564
- fullDescription: this.buildFullDescription(descriptions, testDescription),
1565
- filePath: testData.filePath ?? "<unknown>",
1566
- line: testData.line ?? 0,
1567
- durationMs: Date.now() - testStartMs
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
1568
1543
  })
1569
-
1570
- if (this._abortRemainingTests) break
1544
+ test.options.retries = this.retryCount(test)
1571
1545
  }
1572
1546
 
1573
- for (const subDescription in tests.subs) {
1574
- if (this._abortRemainingTests) break
1575
-
1576
- const subTest = tests.subs[subDescription]
1577
- const newDecriptions = descriptions.concat([subDescription])
1578
- const childScopeLineMatch = scopeLineMatch || this.matchesLineFilter(subTest)
1579
-
1580
- if (!this._onlyFocussed || subTest.anyTestsFocussed) {
1581
- console.log(`${leftPadding}${subDescription}`)
1582
- await this.runTests({
1583
- afterEaches: newAfterEaches,
1584
- beforeEaches: newBeforeEaches,
1585
- tests: subTest,
1586
- descriptions: newDecriptions,
1587
- indentLevel: indentLevel + 1,
1588
- lineMatchedInScope: childScopeLineMatch,
1589
- parentProfileScopeId: profileScopeId
1590
- })
1591
- }
1547
+ for (const childSuite of suite.suites) visit(childSuite)
1548
+ }
1549
+
1550
+ for (const suite of this.getTestContext().registry.suites) visit(suite)
1551
+
1552
+ return () => {
1553
+ for (const restoration of restorations) {
1554
+ if (restoration.hadRetries) restoration.options.retries = restoration.retries
1555
+ else delete restoration.options.retries
1592
1556
  }
1593
- } catch (error) {
1594
- scopeErrors.push(error)
1595
1557
  }
1558
+ }
1559
+
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) }
1566
+
1567
+ /** Records one successful package result. */
1568
+ recordSuccessfulTest() { this._successfulTests++ }
1569
+
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
+ }
1590
+
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 }
1597
+
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
1596
1624
 
1597
1625
  try {
1598
- await this.runAfterAllsForScope(scopeEntry)
1599
- } catch (error) {
1600
- scopeErrors.push(error)
1601
- }
1602
- const scopeIndex = this._activeAfterAllScopes.indexOf(scopeEntry)
1626
+ try {
1627
+ result = await this._packageRunner.run()
1628
+ } catch (error) {
1629
+ if (!(error instanceof AbortRemainingTestsError)) throw error
1603
1630
 
1604
- if (scopeIndex >= 0) {
1605
- this._activeAfterAllScopes.splice(scopeIndex, 1)
1631
+ const afterAll = this.afterAllOutcome(this._suiteHookFailures.slice(failureStart))
1632
+ if (afterAll.failed) this.recordTimeoutCleanupFailure(afterAll.error, "afterAll")
1633
+ return
1634
+ }
1635
+
1636
+ this.recordPackageResult(result)
1637
+ this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
1638
+ } finally {
1639
+ restoreRetryOptions()
1606
1640
  }
1641
+ }
1607
1642
 
1608
- if (scopeErrors.length > 0 && this._abortRemainingTests) {
1609
- const error = scopeErrors.length == 1
1610
- ? scopeErrors[0]
1611
- : 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)
1612
1650
 
1613
- this.recordTimeoutCleanupFailure(error, "afterAll")
1614
- 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]})
1615
1656
  }
1616
- if (scopeErrors.length == 1) throw scopeErrors[0]
1617
- if (scopeErrors.length > 1) throw new AggregateError(scopeErrors, "Test scope and afterAll cleanup failed", {cause: scopeErrors[0]})
1618
1657
  }
1619
1658
 
1620
1659
  /**
1621
- * Runs run after alls for scope.
1622
- * @param {ActiveAfterAllScopeEntry} scopeEntry - Scope entry.
1623
- * @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}
1624
1663
  */
1625
- async runAfterAllsForScope(scopeEntry) {
1626
- if (scopeEntry.afterAllsRun) return
1664
+ throwAfterAllFailures(failures) {
1665
+ const afterAll = this.afterAllOutcome(failures)
1627
1666
 
1628
- scopeEntry.afterAllsRun = true
1667
+ if (afterAll.failed) throw afterAll.error
1668
+ }
1629
1669
 
1630
- const scopeOwnerFilePath = scopeEntry.tests.ownerFilePath ?? scopeEntry.tests.filePath
1631
- const afterAlls = this.profileHookEntries(
1632
- scopeEntry.tests.afterAlls || [],
1633
- scopeEntry.profileScopeId,
1634
- scopeOwnerFilePath
1635
- )
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()
1636
1702
 
1637
- await this._suiteHookExecutor.runAfterAlls({hooks: afterAlls})
1703
+ try {
1704
+ await this.runPackageTests()
1705
+ } finally {
1706
+ this._context = originalContext
1707
+ }
1708
+ }
1709
+
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
+ })
1638
1735
  }
1639
1736
 
1640
1737
  /**
@@ -1760,57 +1857,4 @@ export default class TestRunner {
1760
1857
  }
1761
1858
  }
1762
1859
 
1763
- /**
1764
- * Runs start console capture.
1765
- * @param {object} [args] - Options object.
1766
- * @param {boolean} [args.passthrough] - Whether to pass through to the original console.
1767
- * @returns {() => string} - Stops the capture and returns captured text.
1768
- */
1769
- startConsoleCapture({passthrough = false} = {}) {
1770
- /**
1771
- * Lines.
1772
- * @type {string[]} */
1773
- const lines = []
1774
- /**
1775
- * Console object.
1776
- * @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
1777
- const consoleObject = /** @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */ (console)
1778
- /**
1779
- * Original console methods captured as direct references so stopping restores
1780
- * the exact method that was installed at capture start.
1781
- * @type {Record<ConsoleMethodName, (...args: Array<ReturnType<typeof JSON.parse>>) => void>} */
1782
- const originalConsoleMethods = {
1783
- debug: consoleObject.debug,
1784
- error: consoleObject.error,
1785
- info: consoleObject.info,
1786
- log: consoleObject.log,
1787
- warn: consoleObject.warn
1788
- }
1789
- let stopped = false
1790
- let outputText = ""
1791
-
1792
- for (const methodName of CAPTURED_CONSOLE_METHODS) {
1793
- consoleObject[methodName] = (...args) => {
1794
- lines.push(`[${new Date().toISOString()}] [${methodName}] ${format(...args)}`)
1795
-
1796
- if (passthrough) {
1797
- originalConsoleMethods[methodName].apply(consoleObject, args)
1798
- }
1799
- }
1800
- }
1801
-
1802
- return () => {
1803
- if (!stopped) {
1804
- stopped = true
1805
-
1806
- for (const methodName of CAPTURED_CONSOLE_METHODS) {
1807
- consoleObject[methodName] = originalConsoleMethods[methodName]
1808
- }
1809
-
1810
- outputText = lines.join("\n")
1811
- }
1812
-
1813
- return outputText
1814
- }
1815
- }
1816
1860
  }