@elliemae/encw-leak-runner 1.0.2

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 (261) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/.stylelintignore +4 -0
  3. package/CHANGELOG.md +51 -0
  4. package/README.md +309 -0
  5. package/babel.config.cjs +2 -0
  6. package/bin/leak-runner.ts +9 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/bin/leak-runner.js +792 -0
  9. package/dist/cjs/analysis/thresholdEvaluator.js +46 -0
  10. package/dist/cjs/browser/iframeHeapProfiler.js +46 -0
  11. package/dist/cjs/cli/command.js +16 -0
  12. package/dist/cjs/cli/commands/listCommand.js +47 -0
  13. package/dist/cjs/cli/commands/runCommand.js +111 -0
  14. package/dist/cjs/cli/index.js +42 -0
  15. package/dist/cjs/config/missingRequiredParamError.js +34 -0
  16. package/dist/cjs/config/requiredEnvParams.js +57 -0
  17. package/dist/cjs/config/runnerConfigLoader.js +73 -0
  18. package/dist/cjs/config/runnerConfigSchema.js +40 -0
  19. package/dist/cjs/config/sources/cliOverrideConfigSource.js +44 -0
  20. package/dist/cjs/config/sources/configSource.js +35 -0
  21. package/dist/cjs/config/sources/envVarConfigSource.js +41 -0
  22. package/dist/cjs/config/sources/fileConfigSource.js +62 -0
  23. package/dist/cjs/index.js +52 -0
  24. package/dist/cjs/package.json +7 -0
  25. package/dist/cjs/registry/scenarioRegistry.js +51 -0
  26. package/dist/cjs/reporting/consoleReporter.js +60 -0
  27. package/dist/cjs/reporting/junitReporter.js +75 -0
  28. package/dist/cjs/reporting/reporter.js +16 -0
  29. package/dist/cjs/runner/aiEnhancementStep.js +39 -0
  30. package/dist/cjs/runner/batchRunner.js +76 -0
  31. package/dist/cjs/runner/scenarioRunner.js +165 -0
  32. package/dist/cjs/scenarios/index.js +29 -0
  33. package/dist/cjs/scenarios/one-admin/export-navigation.scenario.js +50 -0
  34. package/dist/cjs/scenarios/one-admin/index.js +27 -0
  35. package/dist/cjs/scenarios/one-admin/page-models/ExportPageModel.js +43 -0
  36. package/dist/cjs/scenarios/one-admin/page-models/SelectSettingsPageModel.js +47 -0
  37. package/dist/cjs/scenarios/one-admin/page-models/index.js +26 -0
  38. package/dist/cjs/types/config.js +27 -0
  39. package/dist/cjs/types/results.js +16 -0
  40. package/dist/cjs/types/scenario.js +16 -0
  41. package/dist/esm/analysis/thresholdEvaluator.js +26 -0
  42. package/dist/esm/browser/iframeHeapProfiler.js +26 -0
  43. package/dist/esm/cli/command.js +0 -0
  44. package/dist/esm/cli/commands/listCommand.js +27 -0
  45. package/dist/esm/cli/commands/runCommand.js +93 -0
  46. package/dist/esm/cli/index.js +22 -0
  47. package/dist/esm/config/missingRequiredParamError.js +14 -0
  48. package/dist/esm/config/requiredEnvParams.js +37 -0
  49. package/dist/esm/config/runnerConfigLoader.js +53 -0
  50. package/dist/esm/config/runnerConfigSchema.js +20 -0
  51. package/dist/esm/config/sources/cliOverrideConfigSource.js +24 -0
  52. package/dist/esm/config/sources/configSource.js +15 -0
  53. package/dist/esm/config/sources/envVarConfigSource.js +21 -0
  54. package/dist/esm/config/sources/fileConfigSource.js +34 -0
  55. package/dist/esm/index.js +35 -0
  56. package/dist/esm/package.json +7 -0
  57. package/dist/esm/registry/scenarioRegistry.js +31 -0
  58. package/dist/esm/reporting/consoleReporter.js +40 -0
  59. package/dist/esm/reporting/junitReporter.js +45 -0
  60. package/dist/esm/reporting/reporter.js +0 -0
  61. package/dist/esm/runner/aiEnhancementStep.js +22 -0
  62. package/dist/esm/runner/batchRunner.js +56 -0
  63. package/dist/esm/runner/scenarioRunner.js +137 -0
  64. package/dist/esm/scenarios/index.js +9 -0
  65. package/dist/esm/scenarios/one-admin/export-navigation.scenario.js +33 -0
  66. package/dist/esm/scenarios/one-admin/index.js +7 -0
  67. package/dist/esm/scenarios/one-admin/page-models/ExportPageModel.js +23 -0
  68. package/dist/esm/scenarios/one-admin/page-models/SelectSettingsPageModel.js +27 -0
  69. package/dist/esm/scenarios/one-admin/page-models/index.js +6 -0
  70. package/dist/esm/types/config.js +7 -0
  71. package/dist/esm/types/results.js +0 -0
  72. package/dist/esm/types/scenario.js +0 -0
  73. package/dist/types/bin/leak-runner.d.ts +2 -0
  74. package/dist/types/lib/analysis/tests/thresholdEvaluator.test.d.ts +1 -0
  75. package/dist/types/lib/analysis/thresholdEvaluator.d.ts +6 -0
  76. package/dist/types/lib/browser/iframeHeapProfiler.d.ts +9 -0
  77. package/dist/types/lib/browser/tests/iframeHeapProfiler.test.d.ts +1 -0
  78. package/dist/types/lib/cli/command.d.ts +17 -0
  79. package/dist/types/lib/cli/commands/listCommand.d.ts +5 -0
  80. package/dist/types/lib/cli/commands/runCommand.d.ts +7 -0
  81. package/dist/types/lib/cli/index.d.ts +4 -0
  82. package/dist/types/lib/config/missingRequiredParamError.d.ts +4 -0
  83. package/dist/types/lib/config/requiredEnvParams.d.ts +16 -0
  84. package/dist/types/lib/config/runnerConfigLoader.d.ts +13 -0
  85. package/dist/types/lib/config/runnerConfigSchema.d.ts +78 -0
  86. package/dist/types/lib/config/sources/cliOverrideConfigSource.d.ts +14 -0
  87. package/dist/types/lib/config/sources/configSource.d.ts +14 -0
  88. package/dist/types/lib/config/sources/envVarConfigSource.d.ts +7 -0
  89. package/dist/types/lib/config/sources/fileConfigSource.d.ts +9 -0
  90. package/dist/types/lib/config/tests/cliOverrideConfigSource.test.d.ts +1 -0
  91. package/dist/types/lib/config/tests/envVarConfigSource.test.d.ts +1 -0
  92. package/dist/types/lib/config/tests/fileConfigSource.test.d.ts +1 -0
  93. package/dist/types/lib/config/tests/requiredEnvParams.test.d.ts +1 -0
  94. package/dist/types/lib/config/tests/runnerConfigLoader.test.d.ts +1 -0
  95. package/dist/types/lib/index.d.ts +18 -0
  96. package/dist/types/lib/registry/scenarioRegistry.d.ts +18 -0
  97. package/dist/types/lib/registry/tests/scenarioRegistry.test.d.ts +1 -0
  98. package/dist/types/lib/reporting/consoleReporter.d.ts +5 -0
  99. package/dist/types/lib/reporting/junitReporter.d.ts +5 -0
  100. package/dist/types/lib/reporting/reporter.d.ts +4 -0
  101. package/dist/types/lib/reporting/tests/consoleReporter.test.d.ts +1 -0
  102. package/dist/types/lib/reporting/tests/junitReporter.test.d.ts +1 -0
  103. package/dist/types/lib/runner/aiEnhancementStep.d.ts +15 -0
  104. package/dist/types/lib/runner/batchRunner.d.ts +14 -0
  105. package/dist/types/lib/runner/scenarioRunner.d.ts +15 -0
  106. package/dist/types/lib/runner/tests/aiEnhancementStep.test.d.ts +1 -0
  107. package/dist/types/lib/runner/tests/batchRunner.test.d.ts +1 -0
  108. package/dist/types/lib/runner/tests/scenarioRunner.test.d.ts +1 -0
  109. package/dist/types/lib/scenarios/index.d.ts +2 -0
  110. package/dist/types/lib/scenarios/one-admin/export-navigation.scenario.d.ts +2 -0
  111. package/dist/types/lib/scenarios/one-admin/index.d.ts +2 -0
  112. package/dist/types/lib/scenarios/one-admin/page-models/ExportPageModel.d.ts +8 -0
  113. package/dist/types/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.d.ts +10 -0
  114. package/dist/types/lib/scenarios/one-admin/page-models/index.d.ts +2 -0
  115. package/dist/types/lib/types/config.d.ts +26 -0
  116. package/dist/types/lib/types/results.d.ts +19 -0
  117. package/dist/types/lib/types/scenario.d.ts +17 -0
  118. package/jest.config.cjs +9 -0
  119. package/leak-runner.config.json +13 -0
  120. package/leak-runner.schema.json +27 -0
  121. package/lib/analysis/tests/thresholdEvaluator.test.ts +125 -0
  122. package/lib/analysis/thresholdEvaluator.ts +36 -0
  123. package/lib/browser/iframeHeapProfiler.ts +30 -0
  124. package/lib/browser/tests/iframeHeapProfiler.test.ts +71 -0
  125. package/lib/cli/command.ts +19 -0
  126. package/lib/cli/commands/listCommand.ts +36 -0
  127. package/lib/cli/commands/runCommand.ts +126 -0
  128. package/lib/cli/index.ts +25 -0
  129. package/lib/config/missingRequiredParamError.ts +10 -0
  130. package/lib/config/requiredEnvParams.ts +50 -0
  131. package/lib/config/runnerConfigLoader.ts +84 -0
  132. package/lib/config/runnerConfigSchema.ts +27 -0
  133. package/lib/config/sources/cliOverrideConfigSource.ts +30 -0
  134. package/lib/config/sources/configSource.ts +27 -0
  135. package/lib/config/sources/envVarConfigSource.ts +23 -0
  136. package/lib/config/sources/fileConfigSource.ts +39 -0
  137. package/lib/config/tests/cliOverrideConfigSource.test.ts +25 -0
  138. package/lib/config/tests/envVarConfigSource.test.ts +57 -0
  139. package/lib/config/tests/fileConfigSource.test.ts +49 -0
  140. package/lib/config/tests/requiredEnvParams.test.ts +113 -0
  141. package/lib/config/tests/runnerConfigLoader.test.ts +59 -0
  142. package/lib/index.ts +37 -0
  143. package/lib/registry/scenarioRegistry.ts +48 -0
  144. package/lib/registry/tests/scenarioRegistry.test.ts +96 -0
  145. package/lib/reporting/consoleReporter.ts +48 -0
  146. package/lib/reporting/junitReporter.ts +62 -0
  147. package/lib/reporting/reporter.ts +5 -0
  148. package/lib/reporting/tests/consoleReporter.test.ts +82 -0
  149. package/lib/reporting/tests/junitReporter.test.ts +103 -0
  150. package/lib/runner/aiEnhancementStep.ts +39 -0
  151. package/lib/runner/batchRunner.ts +71 -0
  152. package/lib/runner/scenarioRunner.ts +189 -0
  153. package/lib/runner/tests/aiEnhancementStep.test.ts +174 -0
  154. package/lib/runner/tests/batchRunner.test.ts +133 -0
  155. package/lib/runner/tests/scenarioRunner.test.ts +162 -0
  156. package/lib/scenarios/index.ts +8 -0
  157. package/lib/scenarios/one-admin/export-navigation.scenario.ts +38 -0
  158. package/lib/scenarios/one-admin/index.ts +6 -0
  159. package/lib/scenarios/one-admin/page-models/ExportPageModel.ts +26 -0
  160. package/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.ts +30 -0
  161. package/lib/scenarios/one-admin/page-models/index.ts +2 -0
  162. package/lib/types/config.ts +34 -0
  163. package/lib/types/results.ts +22 -0
  164. package/lib/types/scenario.ts +18 -0
  165. package/package.json +46 -0
  166. package/reports/analysis/index.html +116 -0
  167. package/reports/analysis/thresholdEvaluator.ts.html +193 -0
  168. package/reports/base.css +224 -0
  169. package/reports/block-navigation.js +87 -0
  170. package/reports/browser/iframeHeapProfiler.ts.html +175 -0
  171. package/reports/browser/index.html +116 -0
  172. package/reports/cli/commands/index.html +131 -0
  173. package/reports/cli/commands/listCommand.ts.html +193 -0
  174. package/reports/cli/commands/runCommand.ts.html +463 -0
  175. package/reports/cli/index.html +116 -0
  176. package/reports/cli/index.ts.html +160 -0
  177. package/reports/config/index.html +161 -0
  178. package/reports/config/missingRequiredParamError.ts.html +115 -0
  179. package/reports/config/requiredEnvParams.ts.html +235 -0
  180. package/reports/config/runnerConfigLoader.ts.html +337 -0
  181. package/reports/config/runnerConfigSchema.ts.html +166 -0
  182. package/reports/config/sources/cliOverrideConfigSource.ts.html +175 -0
  183. package/reports/config/sources/configSource.ts.html +166 -0
  184. package/reports/config/sources/envVarConfigSource.ts.html +154 -0
  185. package/reports/config/sources/fileConfigSource.ts.html +202 -0
  186. package/reports/config/sources/index.html +161 -0
  187. package/reports/favicon.png +0 -0
  188. package/reports/index.html +296 -0
  189. package/reports/lcov-report/analysis/index.html +116 -0
  190. package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +193 -0
  191. package/reports/lcov-report/base.css +224 -0
  192. package/reports/lcov-report/block-navigation.js +87 -0
  193. package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +175 -0
  194. package/reports/lcov-report/browser/index.html +116 -0
  195. package/reports/lcov-report/cli/commands/index.html +131 -0
  196. package/reports/lcov-report/cli/commands/listCommand.ts.html +193 -0
  197. package/reports/lcov-report/cli/commands/runCommand.ts.html +463 -0
  198. package/reports/lcov-report/cli/index.html +116 -0
  199. package/reports/lcov-report/cli/index.ts.html +160 -0
  200. package/reports/lcov-report/config/index.html +161 -0
  201. package/reports/lcov-report/config/missingRequiredParamError.ts.html +115 -0
  202. package/reports/lcov-report/config/requiredEnvParams.ts.html +235 -0
  203. package/reports/lcov-report/config/runnerConfigLoader.ts.html +337 -0
  204. package/reports/lcov-report/config/runnerConfigSchema.ts.html +166 -0
  205. package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +175 -0
  206. package/reports/lcov-report/config/sources/configSource.ts.html +166 -0
  207. package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +154 -0
  208. package/reports/lcov-report/config/sources/fileConfigSource.ts.html +202 -0
  209. package/reports/lcov-report/config/sources/index.html +161 -0
  210. package/reports/lcov-report/favicon.png +0 -0
  211. package/reports/lcov-report/index.html +296 -0
  212. package/reports/lcov-report/prettify.css +1 -0
  213. package/reports/lcov-report/prettify.js +2 -0
  214. package/reports/lcov-report/registry/index.html +116 -0
  215. package/reports/lcov-report/registry/scenarioRegistry.ts.html +229 -0
  216. package/reports/lcov-report/reporting/consoleReporter.ts.html +229 -0
  217. package/reports/lcov-report/reporting/index.html +131 -0
  218. package/reports/lcov-report/reporting/junitReporter.ts.html +271 -0
  219. package/reports/lcov-report/runner/aiEnhancementStep.ts.html +202 -0
  220. package/reports/lcov-report/runner/batchRunner.ts.html +298 -0
  221. package/reports/lcov-report/runner/index.html +146 -0
  222. package/reports/lcov-report/runner/scenarioRunner.ts.html +652 -0
  223. package/reports/lcov-report/scenarios/index.html +116 -0
  224. package/reports/lcov-report/scenarios/index.ts.html +109 -0
  225. package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
  226. package/reports/lcov-report/scenarios/one-admin/index.html +131 -0
  227. package/reports/lcov-report/scenarios/one-admin/index.ts.html +103 -0
  228. package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
  229. package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
  230. package/reports/lcov-report/scenarios/one-admin/page-models/index.html +131 -0
  231. package/reports/lcov-report/sort-arrow-sprite.png +0 -0
  232. package/reports/lcov-report/sorter.js +210 -0
  233. package/reports/lcov-report/types/config.ts.html +187 -0
  234. package/reports/lcov-report/types/index.html +116 -0
  235. package/reports/lcov.info +883 -0
  236. package/reports/prettify.css +1 -0
  237. package/reports/prettify.js +2 -0
  238. package/reports/registry/index.html +116 -0
  239. package/reports/registry/scenarioRegistry.ts.html +229 -0
  240. package/reports/reporting/consoleReporter.ts.html +229 -0
  241. package/reports/reporting/index.html +131 -0
  242. package/reports/reporting/junitReporter.ts.html +271 -0
  243. package/reports/runner/aiEnhancementStep.ts.html +202 -0
  244. package/reports/runner/batchRunner.ts.html +298 -0
  245. package/reports/runner/index.html +146 -0
  246. package/reports/runner/scenarioRunner.ts.html +652 -0
  247. package/reports/scenarios/index.html +116 -0
  248. package/reports/scenarios/index.ts.html +109 -0
  249. package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
  250. package/reports/scenarios/one-admin/index.html +131 -0
  251. package/reports/scenarios/one-admin/index.ts.html +103 -0
  252. package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
  253. package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
  254. package/reports/scenarios/one-admin/page-models/index.html +131 -0
  255. package/reports/sort-arrow-sprite.png +0 -0
  256. package/reports/sorter.js +210 -0
  257. package/reports/types/config.ts.html +187 -0
  258. package/reports/types/index.html +116 -0
  259. package/stylelint.config.cjs +2 -0
  260. package/test-report.xml +100 -0
  261. package/tsconfig.json +12 -0
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ import {
3
+ runnerConfigFileSchema
4
+ } from "../runnerConfigSchema.js";
5
+ class FileConfigSource {
6
+ constructor(filePath) {
7
+ this.filePath = filePath;
8
+ }
9
+ filePath;
10
+ priority = 1;
11
+ name = "file";
12
+ load() {
13
+ if (!fs.existsSync(this.filePath)) return {};
14
+ const raw = fs.readFileSync(this.filePath, "utf8");
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ } catch (err) {
19
+ throw new Error(
20
+ `Failed to parse config file ${this.filePath}: ${err.message}`
21
+ );
22
+ }
23
+ const result = runnerConfigFileSchema.safeParse(parsed);
24
+ if (!result.success) {
25
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
26
+ throw new Error(`Invalid config file ${this.filePath}:
27
+ ${issues}`);
28
+ }
29
+ return result.data;
30
+ }
31
+ }
32
+ export {
33
+ FileConfigSource
34
+ };
@@ -0,0 +1,35 @@
1
+ import { DEFAULT_THRESHOLDS } from "./types/config.js";
2
+ import { ScenarioRegistry } from "./registry/scenarioRegistry.js";
3
+ import { BatchRunner } from "./runner/batchRunner.js";
4
+ import { ScenarioRunner } from "./runner/scenarioRunner.js";
5
+ import { IframeHeapProfiler } from "./browser/iframeHeapProfiler.js";
6
+ import { ThresholdEvaluator } from "./analysis/thresholdEvaluator.js";
7
+ import { ConsoleReporter } from "./reporting/consoleReporter.js";
8
+ import { JunitReporter } from "./reporting/junitReporter.js";
9
+ import { RunnerConfigLoader } from "./config/runnerConfigLoader.js";
10
+ import { FileConfigSource } from "./config/sources/fileConfigSource.js";
11
+ import { EnvVarConfigSource } from "./config/sources/envVarConfigSource.js";
12
+ import { CliOverrideConfigSource } from "./config/sources/cliOverrideConfigSource.js";
13
+ import {
14
+ RequiredEnvParamsResolver,
15
+ MissingRequiredParamError
16
+ } from "./config/requiredEnvParams.js";
17
+ import { buildProgram, defaultDeps } from "./cli/index.js";
18
+ export {
19
+ BatchRunner,
20
+ CliOverrideConfigSource,
21
+ ConsoleReporter,
22
+ DEFAULT_THRESHOLDS,
23
+ EnvVarConfigSource,
24
+ FileConfigSource,
25
+ IframeHeapProfiler,
26
+ JunitReporter,
27
+ MissingRequiredParamError,
28
+ RequiredEnvParamsResolver,
29
+ RunnerConfigLoader,
30
+ ScenarioRegistry,
31
+ ScenarioRunner,
32
+ ThresholdEvaluator,
33
+ buildProgram,
34
+ defaultDeps
35
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "type": "module",
3
+ "sideEffects": false,
4
+ "publishConfig": {
5
+ "access": "public"
6
+ }
7
+ }
@@ -0,0 +1,31 @@
1
+ class ScenarioRegistry {
2
+ entries = /* @__PURE__ */ new Map();
3
+ register(group) {
4
+ group.scenarios.forEach((scenario) => {
5
+ const key = `${group.microapp}/${scenario.id}`;
6
+ if (this.entries.has(key)) {
7
+ throw new Error(`Duplicate scenario: ${key}`);
8
+ }
9
+ this.entries.set(key, scenario);
10
+ });
11
+ return this;
12
+ }
13
+ get(key) {
14
+ return this.entries.get(key);
15
+ }
16
+ has(key) {
17
+ return this.entries.has(key);
18
+ }
19
+ size() {
20
+ return this.entries.size;
21
+ }
22
+ list() {
23
+ return Array.from(this.entries.entries()).map(([key, scenario]) => ({ key, scenario })).sort((a, b) => a.key.localeCompare(b.key));
24
+ }
25
+ filterByTag(tag) {
26
+ return this.list().filter((entry) => entry.scenario.tags?.includes(tag));
27
+ }
28
+ }
29
+ export {
30
+ ScenarioRegistry
31
+ };
@@ -0,0 +1,40 @@
1
+ const GREEN = "\x1B[32m";
2
+ const RED = "\x1B[31m";
3
+ const BOLD = "\x1B[1m";
4
+ const RESET = "\x1B[0m";
5
+ function formatDuration(ms) {
6
+ return `${(ms / 1e3).toFixed(1)}s`;
7
+ }
8
+ function printLine(text) {
9
+ process.stdout.write(`${text}
10
+ `);
11
+ }
12
+ function printScenario(result) {
13
+ const status = result.passed ? `${GREEN}PASSED${RESET}` : `${RED}FAILED${RESET}`;
14
+ const duration = formatDuration(result.durationMs);
15
+ printLine(` ${status} ${BOLD}${result.name}${RESET} (${duration})`);
16
+ if (!result.passed && result.thresholdResult.reason) {
17
+ printLine(` ${RED}\u2192 ${result.thresholdResult.reason}${RESET}`);
18
+ }
19
+ if (!result.passed && result.error) {
20
+ printLine(` ${RED}\u2192 Error: ${result.error.message}${RESET}`);
21
+ }
22
+ }
23
+ class ConsoleReporter {
24
+ write(summary) {
25
+ printLine("");
26
+ printLine(`${BOLD}MEMORY LEAK RUN RESULTS${RESET}`);
27
+ printLine("\u2500".repeat(50));
28
+ summary.results.forEach(printScenario);
29
+ printLine("\u2500".repeat(50));
30
+ const total = summary.results.length;
31
+ const duration = formatDuration(summary.totalDurationMs);
32
+ printLine(
33
+ `${summary.passCount} passed, ${summary.failCount} failed of ${total} scenarios (${duration})`
34
+ );
35
+ printLine("");
36
+ }
37
+ }
38
+ export {
39
+ ConsoleReporter
40
+ };
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function escapeXml(str) {
4
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5
+ }
6
+ function formatSeconds(ms) {
7
+ return (ms / 1e3).toFixed(1);
8
+ }
9
+ function renderTestCase(result) {
10
+ const timeAttr = `time="${formatSeconds(result.durationMs)}"`;
11
+ const open = ` <testcase name="${escapeXml(
12
+ result.name
13
+ )}" classname="memory-leak" ${timeAttr}>`;
14
+ if (result.passed) {
15
+ return `${open}
16
+ </testcase>`;
17
+ }
18
+ const message = result.thresholdResult.reason ?? result.error?.message ?? "Unknown failure";
19
+ const body = result.thresholdResult.reason ? `See leak-reports/${result.name}.md for full analysis` : result.error?.stack ?? "";
20
+ return `${open}
21
+ <failure message="${escapeXml(message)}">
22
+ ${escapeXml(body)}
23
+ </failure>
24
+ </testcase>`;
25
+ }
26
+ class JunitReporter {
27
+ write(summary, outputDir) {
28
+ const totalTime = formatSeconds(summary.totalDurationMs);
29
+ const testCases = summary.results.map(renderTestCase).join("\n");
30
+ const xml = [
31
+ '<?xml version="1.0" encoding="UTF-8"?>',
32
+ `<testsuites name="encw-leak-runner" time="${totalTime}">`,
33
+ ` <testsuite name="microapp-memory-leaks" tests="${summary.results.length}" failures="${summary.failCount}" time="${totalTime}">`,
34
+ testCases,
35
+ " </testsuite>",
36
+ "</testsuites>"
37
+ ].join("\n");
38
+ fs.mkdirSync(outputDir, { recursive: true });
39
+ fs.writeFileSync(path.join(outputDir, "test-results.xml"), xml, "utf8");
40
+ return Promise.resolve();
41
+ }
42
+ }
43
+ export {
44
+ JunitReporter
45
+ };
File without changes
@@ -0,0 +1,22 @@
1
+ import {
2
+ AiEnhancer,
3
+ renderComparisonMarkdown
4
+ } from "@elliemae/encw-heap-doctor";
5
+ async function enhanceMarkdownIfConfigured(report, config, scenarioName) {
6
+ if (!config.ai?.apiKey) return report.markdown;
7
+ try {
8
+ const enhancer = new AiEnhancer(config.ai);
9
+ await enhancer.enhance(report.afterAnalysis.leakResults);
10
+ return renderComparisonMarkdown(report);
11
+ } catch (err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ process.stderr.write(
14
+ `Genice enhancement failed for ${scenarioName}: ${message}; writing report without AI section.
15
+ `
16
+ );
17
+ return report.markdown;
18
+ }
19
+ }
20
+ export {
21
+ enhanceMarkdownIfConfigured
22
+ };
@@ -0,0 +1,56 @@
1
+ import { ScenarioRunner } from "./scenarioRunner.js";
2
+ function buildSummary(results) {
3
+ return {
4
+ results,
5
+ totalDurationMs: results.reduce((acc, r) => acc + r.durationMs, 0),
6
+ passCount: results.filter((r) => r.passed).length,
7
+ failCount: results.filter((r) => !r.passed).length
8
+ };
9
+ }
10
+ class BatchRunner {
11
+ constructor(config, registry) {
12
+ this.config = config;
13
+ this.registry = registry;
14
+ this.scenarioRunner = new ScenarioRunner(config);
15
+ }
16
+ config;
17
+ registry;
18
+ scenarioRunner;
19
+ async runAll() {
20
+ const scenarios = this.registry.list().map((entry) => entry.scenario);
21
+ return this.executeScenarios(scenarios);
22
+ }
23
+ async runByTag(tag) {
24
+ const scenarios = this.registry.filterByTag(tag).map((entry) => entry.scenario);
25
+ return this.executeScenarios(scenarios);
26
+ }
27
+ async runByName(key) {
28
+ const scenario = this.registry.get(key);
29
+ if (!scenario) {
30
+ throw new Error(`No scenario registered with key "${key}"`);
31
+ }
32
+ return this.executeScenarios([scenario]);
33
+ }
34
+ async executeScenarios(scenarios) {
35
+ const results = await scenarios.reduce(
36
+ async (acc, scenario) => {
37
+ const prior = await acc;
38
+ const result = await this.runOne(scenario);
39
+ return [...prior, result];
40
+ },
41
+ Promise.resolve([])
42
+ );
43
+ return buildSummary(results);
44
+ }
45
+ async runOne(scenario) {
46
+ const browser = await this.scenarioRunner.launchBrowser();
47
+ try {
48
+ return await this.scenarioRunner.run(scenario, browser);
49
+ } finally {
50
+ await browser.close();
51
+ }
52
+ }
53
+ }
54
+ export {
55
+ BatchRunner
56
+ };
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ chromium
5
+ } from "@playwright/test";
6
+ import { AuthManager, PageSetup } from "@elliemae/smoked-suite";
7
+ import { IframeHeapProfiler } from "../browser/iframeHeapProfiler.js";
8
+ import { ThresholdEvaluator } from "../analysis/thresholdEvaluator.js";
9
+ import { DEFAULT_THRESHOLDS } from "../types/config.js";
10
+ import { enhanceMarkdownIfConfigured } from "./aiEnhancementStep.js";
11
+ async function resolveIframe(page, selector) {
12
+ const handle = await page.locator(selector).elementHandle();
13
+ if (!handle) throw new Error(`Iframe not found for selector: ${selector}`);
14
+ const frame = await handle.contentFrame();
15
+ if (!frame)
16
+ throw new Error(`Could not get content frame for selector: ${selector}`);
17
+ return frame;
18
+ }
19
+ async function forceGarbageCollection(page) {
20
+ const session = await page.context().newCDPSession(page);
21
+ await session.send("HeapProfiler.enable");
22
+ await session.send("HeapProfiler.collectGarbage");
23
+ await session.send("HeapProfiler.disable");
24
+ await session.detach();
25
+ await page.waitForTimeout(2e3);
26
+ }
27
+ function cleanupSnapshots(paths) {
28
+ if (paths.before && fs.existsSync(paths.before)) fs.unlinkSync(paths.before);
29
+ if (paths.after && fs.existsSync(paths.after)) fs.unlinkSync(paths.after);
30
+ }
31
+ class ScenarioRunner {
32
+ constructor(config) {
33
+ this.config = config;
34
+ }
35
+ config;
36
+ async launchBrowser() {
37
+ return chromium.launch({ headless: this.config.runner.headless });
38
+ }
39
+ async run(scenario, browser) {
40
+ const startTime = Date.now();
41
+ const context = await browser.newContext({
42
+ baseURL: this.config.env.baseUrl
43
+ });
44
+ const page = await context.newPage();
45
+ const paths = { before: "", after: "" };
46
+ let report = null;
47
+ let error = null;
48
+ try {
49
+ report = await this.runScenarioInPage(page, scenario, paths);
50
+ } catch (err) {
51
+ error = err instanceof Error ? err : new Error(String(err));
52
+ } finally {
53
+ await context.close();
54
+ cleanupSnapshots(paths);
55
+ }
56
+ return this.buildResult({ scenario, report, error, startTime });
57
+ }
58
+ async runScenarioInPage(page, scenario, paths) {
59
+ const snapshotsDir = path.join(this.config.runner.outputDir, "snapshots");
60
+ const { profiler, frame } = await this.setupAndProfile(
61
+ page,
62
+ scenario,
63
+ snapshotsDir
64
+ );
65
+ paths.before = await profiler.captureSnapshot("before");
66
+ await this.repeatScenarioActions(scenario, page, frame);
67
+ await forceGarbageCollection(page);
68
+ paths.after = await profiler.captureSnapshot("after");
69
+ const report = await profiler.compare(
70
+ "before",
71
+ "after",
72
+ this.config.runner.topN
73
+ );
74
+ if (report) await this.writeReport(report, scenario);
75
+ return report;
76
+ }
77
+ async setupAndProfile(page, scenario, snapshotsDir) {
78
+ const auth = new AuthManager();
79
+ const pageSetup = new PageSetup();
80
+ await pageSetup.apply(page);
81
+ await auth.login(page, {
82
+ username: this.config.env.userId,
83
+ password: this.config.env.password,
84
+ instanceId: this.config.env.instanceId
85
+ });
86
+ await page.goto(scenario.url());
87
+ const frame = await resolveIframe(page, scenario.microappSelector);
88
+ fs.mkdirSync(snapshotsDir, { recursive: true });
89
+ const profiler = new IframeHeapProfiler(page, frame, snapshotsDir);
90
+ return { profiler, frame };
91
+ }
92
+ async repeatScenarioActions(scenario, page, frame) {
93
+ const repeatCount = scenario.repeat?.() ?? 3;
94
+ for (let i = 0; i < repeatCount; i += 1) {
95
+ await scenario.action(page, frame);
96
+ if (scenario.back) {
97
+ await scenario.back(page);
98
+ }
99
+ }
100
+ }
101
+ async writeReport(report, scenario) {
102
+ const reportPath = path.join(
103
+ this.config.runner.outputDir,
104
+ `${scenario.id}.md`
105
+ );
106
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
107
+ const markdown = await enhanceMarkdownIfConfigured(
108
+ report,
109
+ this.config,
110
+ scenario.name
111
+ );
112
+ fs.writeFileSync(reportPath, markdown);
113
+ }
114
+ buildResult(input) {
115
+ const { scenario, report, error, startTime } = input;
116
+ const thresholds = {
117
+ ...DEFAULT_THRESHOLDS,
118
+ ...scenario.thresholds
119
+ };
120
+ const failureFallback = {
121
+ passed: false,
122
+ reason: error?.message ?? "Snapshot capture failed"
123
+ };
124
+ const thresholdResult = report && !error ? ThresholdEvaluator.evaluate(report, thresholds) : failureFallback;
125
+ return {
126
+ name: scenario.name,
127
+ passed: thresholdResult.passed,
128
+ thresholdResult,
129
+ report,
130
+ durationMs: Date.now() - startTime,
131
+ error
132
+ };
133
+ }
134
+ }
135
+ export {
136
+ ScenarioRunner
137
+ };
@@ -0,0 +1,9 @@
1
+ import { ScenarioRegistry } from "../registry/scenarioRegistry.js";
2
+ import { oneAdminScenarios } from "./one-admin/index.js";
3
+ const scenarioRegistry = new ScenarioRegistry().register({
4
+ microapp: "one-admin",
5
+ scenarios: oneAdminScenarios
6
+ });
7
+ export {
8
+ scenarioRegistry
9
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ SelectSettingsPageModel,
3
+ ExportPageModel
4
+ } from "./page-models/index.js";
5
+ const exportNavigationScenario = {
6
+ id: "export-navigation",
7
+ name: "Export Navigation",
8
+ description: "Navigate to export page and come back - verify iframe GC",
9
+ tags: ["critical"],
10
+ microappSelector: "iframe#pui-iframe-container-emAdminUI",
11
+ url: () => "/admin/oneadmin/migrate",
12
+ async action(page, frame) {
13
+ const settings = new SelectSettingsPageModel(frame);
14
+ await settings.container.waitFor({ state: "visible" });
15
+ await settings.expandTreeItem("eFolder");
16
+ await settings.selectTreeItem("Enhanced Conditions");
17
+ await settings.clickExport();
18
+ const exportPage = new ExportPageModel(frame);
19
+ await exportPage.sourceDataTab.waitFor({ state: "visible" });
20
+ await exportPage.dataTable.waitFor({ state: "visible" });
21
+ },
22
+ async back(page) {
23
+ await ExportPageModel.clickBackAndWaitForSettings(page);
24
+ },
25
+ repeat: () => 3,
26
+ thresholds: {
27
+ maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
28
+ maxNewLeakGroups: 0
29
+ }
30
+ };
31
+ export {
32
+ exportNavigationScenario
33
+ };
@@ -0,0 +1,7 @@
1
+ import { exportNavigationScenario } from "./export-navigation.scenario.js";
2
+ const oneAdminScenarios = [
3
+ exportNavigationScenario
4
+ ];
5
+ export {
6
+ oneAdminScenarios
7
+ };
@@ -0,0 +1,23 @@
1
+ const IFRAME_SELECTOR = "iframe#pui-iframe-container-emAdminUI";
2
+ const SETTINGS_URL_PATTERN = /\/admin\/oneadmin\/migrate(?:\/)?$/;
3
+ class ExportPageModel {
4
+ constructor(frame) {
5
+ this.frame = frame;
6
+ }
7
+ frame;
8
+ get sourceDataTab() {
9
+ return this.frame.getByRole("tab", { name: /Source Data/i });
10
+ }
11
+ get dataTable() {
12
+ return this.frame.locator('[role="table"]').first();
13
+ }
14
+ static async clickBackAndWaitForSettings(page) {
15
+ await Promise.all([
16
+ page.waitForURL(SETTINGS_URL_PATTERN),
17
+ page.frameLocator(IFRAME_SELECTOR).getByRole("button", { name: /back/i }).click()
18
+ ]);
19
+ }
20
+ }
21
+ export {
22
+ ExportPageModel
23
+ };
@@ -0,0 +1,27 @@
1
+ class SelectSettingsPageModel {
2
+ constructor(frame) {
3
+ this.frame = frame;
4
+ }
5
+ frame;
6
+ get container() {
7
+ return this.frame.locator("#one-admin-console-container");
8
+ }
9
+ get exportButton() {
10
+ return this.frame.getByRole("button", { name: "Export" });
11
+ }
12
+ async expandTreeItem(name) {
13
+ const treeItem = this.frame.locator(
14
+ `[role="treeitem"]:has-text("${name}")`
15
+ );
16
+ await treeItem.locator('button[aria-label*="expand"], button[aria-label*="toggle"]').click();
17
+ }
18
+ async selectTreeItem(name) {
19
+ await this.frame.locator(`[role="treeitem"]:has-text("${name}")`).click();
20
+ }
21
+ async clickExport() {
22
+ await this.exportButton.click();
23
+ }
24
+ }
25
+ export {
26
+ SelectSettingsPageModel
27
+ };
@@ -0,0 +1,6 @@
1
+ import { SelectSettingsPageModel } from "./SelectSettingsPageModel.js";
2
+ import { ExportPageModel } from "./ExportPageModel.js";
3
+ export {
4
+ ExportPageModel,
5
+ SelectSettingsPageModel
6
+ };
@@ -0,0 +1,7 @@
1
+ const DEFAULT_THRESHOLDS = {
2
+ maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
3
+ maxNewLeakGroups: 0
4
+ };
5
+ export {
6
+ DEFAULT_THRESHOLDS
7
+ };
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ComparisonReport } from '@elliemae/encw-heap-doctor';
2
+ import type { ResolvedThresholds } from '../types/config.js';
3
+ import type { ThresholdResult } from '../types/results.js';
4
+ export declare class ThresholdEvaluator {
5
+ static evaluate(report: ComparisonReport, thresholds: ResolvedThresholds): ThresholdResult;
6
+ }
@@ -0,0 +1,9 @@
1
+ import { HeapMemoryProfiler } from '@elliemae/smoked-suite';
2
+ import type { Page, Frame } from '@playwright/test';
3
+ export declare class IframeHeapProfiler extends HeapMemoryProfiler {
4
+ private readonly profiledPage;
5
+ private readonly frame;
6
+ constructor(profiledPage: Page, frame: Frame, outputDir: string);
7
+ protected getCDPTarget(): Page | Frame;
8
+ private isSameOriginAsPage;
9
+ }
@@ -0,0 +1,17 @@
1
+ import type { Command } from 'commander';
2
+ import type { ScenarioRegistry } from '../registry/scenarioRegistry.js';
3
+ import type { RequiredEnvParamsResolver } from '../config/requiredEnvParams.js';
4
+ /**
5
+ * Dependencies a CliCommand needs from the surrounding wiring.
6
+ *
7
+ * `RunnerConfigLoader` is intentionally NOT here — each command invocation
8
+ * builds its own loader so it can layer in command-line flags (which are only
9
+ * known per-invocation).
10
+ */
11
+ export interface CliDependencies {
12
+ readonly registry: ScenarioRegistry;
13
+ readonly envParams: RequiredEnvParamsResolver;
14
+ }
15
+ export interface CliCommand {
16
+ register(program: Command, deps: CliDependencies): void;
17
+ }
@@ -0,0 +1,5 @@
1
+ import type { Command } from 'commander';
2
+ import type { CliCommand, CliDependencies } from '../command.js';
3
+ export declare class ListCommand implements CliCommand {
4
+ register(program: Command, deps: CliDependencies): void;
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from 'commander';
2
+ import type { CliCommand, CliDependencies } from '../command.js';
3
+ export declare class RunCommand implements CliCommand {
4
+ register(program: Command, deps: CliDependencies): void;
5
+ private buildConfig;
6
+ private selectSummary;
7
+ }
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import type { CliDependencies } from './command.js';
3
+ export declare function buildProgram(deps?: CliDependencies): Command;
4
+ export declare function defaultDeps(): CliDependencies;
@@ -0,0 +1,4 @@
1
+ export declare class MissingRequiredParamError extends Error {
2
+ readonly missing: readonly string[];
3
+ constructor(missing: readonly string[]);
4
+ }
@@ -0,0 +1,16 @@
1
+ import type { EnvironmentParams } from '../types/config.js';
2
+ import { MissingRequiredParamError } from './missingRequiredParamError.js';
3
+ export interface CliEnvOpts {
4
+ readonly baseUrl?: string;
5
+ readonly instanceId?: string;
6
+ readonly userId?: string;
7
+ }
8
+ export { MissingRequiredParamError };
9
+ export declare class RequiredEnvParamsResolver {
10
+ resolve(cliOpts: CliEnvOpts): EnvironmentParams;
11
+ private resolveBaseUrl;
12
+ private resolveInstanceId;
13
+ private resolveUserId;
14
+ private resolvePassword;
15
+ private collectMissing;
16
+ }
@@ -0,0 +1,13 @@
1
+ import type { RunnerOptions } from '../types/config.js';
2
+ import type { ConfigSource } from './sources/configSource.js';
3
+ export interface ResolvedRunnerConfigPartial {
4
+ readonly runner: RunnerOptions;
5
+ readonly aiEnabled: boolean;
6
+ readonly aiModel: string;
7
+ readonly aiTemperature: number;
8
+ }
9
+ export declare class RunnerConfigLoader {
10
+ private readonly sources;
11
+ constructor(sources: readonly ConfigSource[]);
12
+ resolveOptions(): ResolvedRunnerConfigPartial;
13
+ }