@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.
- package/.eslintrc.cjs +10 -0
- package/.stylelintignore +4 -0
- package/CHANGELOG.md +51 -0
- package/README.md +309 -0
- package/babel.config.cjs +2 -0
- package/bin/leak-runner.ts +9 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin/leak-runner.js +792 -0
- package/dist/cjs/analysis/thresholdEvaluator.js +46 -0
- package/dist/cjs/browser/iframeHeapProfiler.js +46 -0
- package/dist/cjs/cli/command.js +16 -0
- package/dist/cjs/cli/commands/listCommand.js +47 -0
- package/dist/cjs/cli/commands/runCommand.js +111 -0
- package/dist/cjs/cli/index.js +42 -0
- package/dist/cjs/config/missingRequiredParamError.js +34 -0
- package/dist/cjs/config/requiredEnvParams.js +57 -0
- package/dist/cjs/config/runnerConfigLoader.js +73 -0
- package/dist/cjs/config/runnerConfigSchema.js +40 -0
- package/dist/cjs/config/sources/cliOverrideConfigSource.js +44 -0
- package/dist/cjs/config/sources/configSource.js +35 -0
- package/dist/cjs/config/sources/envVarConfigSource.js +41 -0
- package/dist/cjs/config/sources/fileConfigSource.js +62 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/package.json +7 -0
- package/dist/cjs/registry/scenarioRegistry.js +51 -0
- package/dist/cjs/reporting/consoleReporter.js +60 -0
- package/dist/cjs/reporting/junitReporter.js +75 -0
- package/dist/cjs/reporting/reporter.js +16 -0
- package/dist/cjs/runner/aiEnhancementStep.js +39 -0
- package/dist/cjs/runner/batchRunner.js +76 -0
- package/dist/cjs/runner/scenarioRunner.js +165 -0
- package/dist/cjs/scenarios/index.js +29 -0
- package/dist/cjs/scenarios/one-admin/export-navigation.scenario.js +50 -0
- package/dist/cjs/scenarios/one-admin/index.js +27 -0
- package/dist/cjs/scenarios/one-admin/page-models/ExportPageModel.js +43 -0
- package/dist/cjs/scenarios/one-admin/page-models/SelectSettingsPageModel.js +47 -0
- package/dist/cjs/scenarios/one-admin/page-models/index.js +26 -0
- package/dist/cjs/types/config.js +27 -0
- package/dist/cjs/types/results.js +16 -0
- package/dist/cjs/types/scenario.js +16 -0
- package/dist/esm/analysis/thresholdEvaluator.js +26 -0
- package/dist/esm/browser/iframeHeapProfiler.js +26 -0
- package/dist/esm/cli/command.js +0 -0
- package/dist/esm/cli/commands/listCommand.js +27 -0
- package/dist/esm/cli/commands/runCommand.js +93 -0
- package/dist/esm/cli/index.js +22 -0
- package/dist/esm/config/missingRequiredParamError.js +14 -0
- package/dist/esm/config/requiredEnvParams.js +37 -0
- package/dist/esm/config/runnerConfigLoader.js +53 -0
- package/dist/esm/config/runnerConfigSchema.js +20 -0
- package/dist/esm/config/sources/cliOverrideConfigSource.js +24 -0
- package/dist/esm/config/sources/configSource.js +15 -0
- package/dist/esm/config/sources/envVarConfigSource.js +21 -0
- package/dist/esm/config/sources/fileConfigSource.js +34 -0
- package/dist/esm/index.js +35 -0
- package/dist/esm/package.json +7 -0
- package/dist/esm/registry/scenarioRegistry.js +31 -0
- package/dist/esm/reporting/consoleReporter.js +40 -0
- package/dist/esm/reporting/junitReporter.js +45 -0
- package/dist/esm/reporting/reporter.js +0 -0
- package/dist/esm/runner/aiEnhancementStep.js +22 -0
- package/dist/esm/runner/batchRunner.js +56 -0
- package/dist/esm/runner/scenarioRunner.js +137 -0
- package/dist/esm/scenarios/index.js +9 -0
- package/dist/esm/scenarios/one-admin/export-navigation.scenario.js +33 -0
- package/dist/esm/scenarios/one-admin/index.js +7 -0
- package/dist/esm/scenarios/one-admin/page-models/ExportPageModel.js +23 -0
- package/dist/esm/scenarios/one-admin/page-models/SelectSettingsPageModel.js +27 -0
- package/dist/esm/scenarios/one-admin/page-models/index.js +6 -0
- package/dist/esm/types/config.js +7 -0
- package/dist/esm/types/results.js +0 -0
- package/dist/esm/types/scenario.js +0 -0
- package/dist/types/bin/leak-runner.d.ts +2 -0
- package/dist/types/lib/analysis/tests/thresholdEvaluator.test.d.ts +1 -0
- package/dist/types/lib/analysis/thresholdEvaluator.d.ts +6 -0
- package/dist/types/lib/browser/iframeHeapProfiler.d.ts +9 -0
- package/dist/types/lib/browser/tests/iframeHeapProfiler.test.d.ts +1 -0
- package/dist/types/lib/cli/command.d.ts +17 -0
- package/dist/types/lib/cli/commands/listCommand.d.ts +5 -0
- package/dist/types/lib/cli/commands/runCommand.d.ts +7 -0
- package/dist/types/lib/cli/index.d.ts +4 -0
- package/dist/types/lib/config/missingRequiredParamError.d.ts +4 -0
- package/dist/types/lib/config/requiredEnvParams.d.ts +16 -0
- package/dist/types/lib/config/runnerConfigLoader.d.ts +13 -0
- package/dist/types/lib/config/runnerConfigSchema.d.ts +78 -0
- package/dist/types/lib/config/sources/cliOverrideConfigSource.d.ts +14 -0
- package/dist/types/lib/config/sources/configSource.d.ts +14 -0
- package/dist/types/lib/config/sources/envVarConfigSource.d.ts +7 -0
- package/dist/types/lib/config/sources/fileConfigSource.d.ts +9 -0
- package/dist/types/lib/config/tests/cliOverrideConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/envVarConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/fileConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/requiredEnvParams.test.d.ts +1 -0
- package/dist/types/lib/config/tests/runnerConfigLoader.test.d.ts +1 -0
- package/dist/types/lib/index.d.ts +18 -0
- package/dist/types/lib/registry/scenarioRegistry.d.ts +18 -0
- package/dist/types/lib/registry/tests/scenarioRegistry.test.d.ts +1 -0
- package/dist/types/lib/reporting/consoleReporter.d.ts +5 -0
- package/dist/types/lib/reporting/junitReporter.d.ts +5 -0
- package/dist/types/lib/reporting/reporter.d.ts +4 -0
- package/dist/types/lib/reporting/tests/consoleReporter.test.d.ts +1 -0
- package/dist/types/lib/reporting/tests/junitReporter.test.d.ts +1 -0
- package/dist/types/lib/runner/aiEnhancementStep.d.ts +15 -0
- package/dist/types/lib/runner/batchRunner.d.ts +14 -0
- package/dist/types/lib/runner/scenarioRunner.d.ts +15 -0
- package/dist/types/lib/runner/tests/aiEnhancementStep.test.d.ts +1 -0
- package/dist/types/lib/runner/tests/batchRunner.test.d.ts +1 -0
- package/dist/types/lib/runner/tests/scenarioRunner.test.d.ts +1 -0
- package/dist/types/lib/scenarios/index.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/export-navigation.scenario.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/index.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/page-models/ExportPageModel.d.ts +8 -0
- package/dist/types/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.d.ts +10 -0
- package/dist/types/lib/scenarios/one-admin/page-models/index.d.ts +2 -0
- package/dist/types/lib/types/config.d.ts +26 -0
- package/dist/types/lib/types/results.d.ts +19 -0
- package/dist/types/lib/types/scenario.d.ts +17 -0
- package/jest.config.cjs +9 -0
- package/leak-runner.config.json +13 -0
- package/leak-runner.schema.json +27 -0
- package/lib/analysis/tests/thresholdEvaluator.test.ts +125 -0
- package/lib/analysis/thresholdEvaluator.ts +36 -0
- package/lib/browser/iframeHeapProfiler.ts +30 -0
- package/lib/browser/tests/iframeHeapProfiler.test.ts +71 -0
- package/lib/cli/command.ts +19 -0
- package/lib/cli/commands/listCommand.ts +36 -0
- package/lib/cli/commands/runCommand.ts +126 -0
- package/lib/cli/index.ts +25 -0
- package/lib/config/missingRequiredParamError.ts +10 -0
- package/lib/config/requiredEnvParams.ts +50 -0
- package/lib/config/runnerConfigLoader.ts +84 -0
- package/lib/config/runnerConfigSchema.ts +27 -0
- package/lib/config/sources/cliOverrideConfigSource.ts +30 -0
- package/lib/config/sources/configSource.ts +27 -0
- package/lib/config/sources/envVarConfigSource.ts +23 -0
- package/lib/config/sources/fileConfigSource.ts +39 -0
- package/lib/config/tests/cliOverrideConfigSource.test.ts +25 -0
- package/lib/config/tests/envVarConfigSource.test.ts +57 -0
- package/lib/config/tests/fileConfigSource.test.ts +49 -0
- package/lib/config/tests/requiredEnvParams.test.ts +113 -0
- package/lib/config/tests/runnerConfigLoader.test.ts +59 -0
- package/lib/index.ts +37 -0
- package/lib/registry/scenarioRegistry.ts +48 -0
- package/lib/registry/tests/scenarioRegistry.test.ts +96 -0
- package/lib/reporting/consoleReporter.ts +48 -0
- package/lib/reporting/junitReporter.ts +62 -0
- package/lib/reporting/reporter.ts +5 -0
- package/lib/reporting/tests/consoleReporter.test.ts +82 -0
- package/lib/reporting/tests/junitReporter.test.ts +103 -0
- package/lib/runner/aiEnhancementStep.ts +39 -0
- package/lib/runner/batchRunner.ts +71 -0
- package/lib/runner/scenarioRunner.ts +189 -0
- package/lib/runner/tests/aiEnhancementStep.test.ts +174 -0
- package/lib/runner/tests/batchRunner.test.ts +133 -0
- package/lib/runner/tests/scenarioRunner.test.ts +162 -0
- package/lib/scenarios/index.ts +8 -0
- package/lib/scenarios/one-admin/export-navigation.scenario.ts +38 -0
- package/lib/scenarios/one-admin/index.ts +6 -0
- package/lib/scenarios/one-admin/page-models/ExportPageModel.ts +26 -0
- package/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.ts +30 -0
- package/lib/scenarios/one-admin/page-models/index.ts +2 -0
- package/lib/types/config.ts +34 -0
- package/lib/types/results.ts +22 -0
- package/lib/types/scenario.ts +18 -0
- package/package.json +46 -0
- package/reports/analysis/index.html +116 -0
- package/reports/analysis/thresholdEvaluator.ts.html +193 -0
- package/reports/base.css +224 -0
- package/reports/block-navigation.js +87 -0
- package/reports/browser/iframeHeapProfiler.ts.html +175 -0
- package/reports/browser/index.html +116 -0
- package/reports/cli/commands/index.html +131 -0
- package/reports/cli/commands/listCommand.ts.html +193 -0
- package/reports/cli/commands/runCommand.ts.html +463 -0
- package/reports/cli/index.html +116 -0
- package/reports/cli/index.ts.html +160 -0
- package/reports/config/index.html +161 -0
- package/reports/config/missingRequiredParamError.ts.html +115 -0
- package/reports/config/requiredEnvParams.ts.html +235 -0
- package/reports/config/runnerConfigLoader.ts.html +337 -0
- package/reports/config/runnerConfigSchema.ts.html +166 -0
- package/reports/config/sources/cliOverrideConfigSource.ts.html +175 -0
- package/reports/config/sources/configSource.ts.html +166 -0
- package/reports/config/sources/envVarConfigSource.ts.html +154 -0
- package/reports/config/sources/fileConfigSource.ts.html +202 -0
- package/reports/config/sources/index.html +161 -0
- package/reports/favicon.png +0 -0
- package/reports/index.html +296 -0
- package/reports/lcov-report/analysis/index.html +116 -0
- package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +193 -0
- package/reports/lcov-report/base.css +224 -0
- package/reports/lcov-report/block-navigation.js +87 -0
- package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +175 -0
- package/reports/lcov-report/browser/index.html +116 -0
- package/reports/lcov-report/cli/commands/index.html +131 -0
- package/reports/lcov-report/cli/commands/listCommand.ts.html +193 -0
- package/reports/lcov-report/cli/commands/runCommand.ts.html +463 -0
- package/reports/lcov-report/cli/index.html +116 -0
- package/reports/lcov-report/cli/index.ts.html +160 -0
- package/reports/lcov-report/config/index.html +161 -0
- package/reports/lcov-report/config/missingRequiredParamError.ts.html +115 -0
- package/reports/lcov-report/config/requiredEnvParams.ts.html +235 -0
- package/reports/lcov-report/config/runnerConfigLoader.ts.html +337 -0
- package/reports/lcov-report/config/runnerConfigSchema.ts.html +166 -0
- package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +175 -0
- package/reports/lcov-report/config/sources/configSource.ts.html +166 -0
- package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +154 -0
- package/reports/lcov-report/config/sources/fileConfigSource.ts.html +202 -0
- package/reports/lcov-report/config/sources/index.html +161 -0
- package/reports/lcov-report/favicon.png +0 -0
- package/reports/lcov-report/index.html +296 -0
- package/reports/lcov-report/prettify.css +1 -0
- package/reports/lcov-report/prettify.js +2 -0
- package/reports/lcov-report/registry/index.html +116 -0
- package/reports/lcov-report/registry/scenarioRegistry.ts.html +229 -0
- package/reports/lcov-report/reporting/consoleReporter.ts.html +229 -0
- package/reports/lcov-report/reporting/index.html +131 -0
- package/reports/lcov-report/reporting/junitReporter.ts.html +271 -0
- package/reports/lcov-report/runner/aiEnhancementStep.ts.html +202 -0
- package/reports/lcov-report/runner/batchRunner.ts.html +298 -0
- package/reports/lcov-report/runner/index.html +146 -0
- package/reports/lcov-report/runner/scenarioRunner.ts.html +652 -0
- package/reports/lcov-report/scenarios/index.html +116 -0
- package/reports/lcov-report/scenarios/index.ts.html +109 -0
- package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
- package/reports/lcov-report/scenarios/one-admin/index.html +131 -0
- package/reports/lcov-report/scenarios/one-admin/index.ts.html +103 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/index.html +131 -0
- package/reports/lcov-report/sort-arrow-sprite.png +0 -0
- package/reports/lcov-report/sorter.js +210 -0
- package/reports/lcov-report/types/config.ts.html +187 -0
- package/reports/lcov-report/types/index.html +116 -0
- package/reports/lcov.info +883 -0
- package/reports/prettify.css +1 -0
- package/reports/prettify.js +2 -0
- package/reports/registry/index.html +116 -0
- package/reports/registry/scenarioRegistry.ts.html +229 -0
- package/reports/reporting/consoleReporter.ts.html +229 -0
- package/reports/reporting/index.html +131 -0
- package/reports/reporting/junitReporter.ts.html +271 -0
- package/reports/runner/aiEnhancementStep.ts.html +202 -0
- package/reports/runner/batchRunner.ts.html +298 -0
- package/reports/runner/index.html +146 -0
- package/reports/runner/scenarioRunner.ts.html +652 -0
- package/reports/scenarios/index.html +116 -0
- package/reports/scenarios/index.ts.html +109 -0
- package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
- package/reports/scenarios/one-admin/index.html +131 -0
- package/reports/scenarios/one-admin/index.ts.html +103 -0
- package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
- package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
- package/reports/scenarios/one-admin/page-models/index.html +131 -0
- package/reports/sort-arrow-sprite.png +0 -0
- package/reports/sorter.js +210 -0
- package/reports/types/config.ts.html +187 -0
- package/reports/types/index.html +116 -0
- package/stylelint.config.cjs +2 -0
- package/test-report.xml +100 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// lib/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// lib/runner/scenarioRunner.ts
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
chromium
|
|
11
|
+
} from "@playwright/test";
|
|
12
|
+
import { AuthManager, PageSetup } from "@elliemae/smoked-suite";
|
|
13
|
+
|
|
14
|
+
// lib/browser/iframeHeapProfiler.ts
|
|
15
|
+
import { HeapMemoryProfiler } from "@elliemae/smoked-suite";
|
|
16
|
+
var IframeHeapProfiler = class extends HeapMemoryProfiler {
|
|
17
|
+
constructor(profiledPage, frame, outputDir) {
|
|
18
|
+
super(profiledPage, outputDir);
|
|
19
|
+
this.profiledPage = profiledPage;
|
|
20
|
+
this.frame = frame;
|
|
21
|
+
}
|
|
22
|
+
getCDPTarget() {
|
|
23
|
+
return this.isSameOriginAsPage(this.frame) ? this.profiledPage : this.frame;
|
|
24
|
+
}
|
|
25
|
+
isSameOriginAsPage(frame) {
|
|
26
|
+
const frameUrl = frame.url();
|
|
27
|
+
const pageUrl = this.profiledPage.url();
|
|
28
|
+
if (!frameUrl || frameUrl === "about:blank") return true;
|
|
29
|
+
try {
|
|
30
|
+
return new URL(frameUrl).origin === new URL(pageUrl).origin;
|
|
31
|
+
} catch {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// lib/analysis/thresholdEvaluator.ts
|
|
38
|
+
function toMB(bytes) {
|
|
39
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
40
|
+
}
|
|
41
|
+
var ThresholdEvaluator = class {
|
|
42
|
+
static evaluate(report, thresholds) {
|
|
43
|
+
const reasons = [];
|
|
44
|
+
if (report.delta.retainedSizeDelta > thresholds.maxRetainedSizeDeltaBytes) {
|
|
45
|
+
reasons.push(
|
|
46
|
+
`Retained size delta ${toMB(report.delta.retainedSizeDelta)} MB exceeds threshold ${toMB(thresholds.maxRetainedSizeDeltaBytes)} MB`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const newLeakCount = report.delta.newLeakGroups.length;
|
|
50
|
+
if (newLeakCount > thresholds.maxNewLeakGroups) {
|
|
51
|
+
reasons.push(
|
|
52
|
+
`${newLeakCount} new leak groups detected (threshold: ${thresholds.maxNewLeakGroups})`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (reasons.length === 0) {
|
|
56
|
+
return { passed: true, reason: null };
|
|
57
|
+
}
|
|
58
|
+
return { passed: false, reason: reasons.join("; ") };
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// lib/types/config.ts
|
|
63
|
+
var DEFAULT_THRESHOLDS = {
|
|
64
|
+
maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
|
|
65
|
+
maxNewLeakGroups: 0
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// lib/runner/aiEnhancementStep.ts
|
|
69
|
+
import {
|
|
70
|
+
AiEnhancer,
|
|
71
|
+
renderComparisonMarkdown
|
|
72
|
+
} from "@elliemae/encw-heap-doctor";
|
|
73
|
+
async function enhanceMarkdownIfConfigured(report, config, scenarioName) {
|
|
74
|
+
if (!config.ai?.apiKey) return report.markdown;
|
|
75
|
+
try {
|
|
76
|
+
const enhancer = new AiEnhancer(config.ai);
|
|
77
|
+
await enhancer.enhance(report.afterAnalysis.leakResults);
|
|
78
|
+
return renderComparisonMarkdown(report);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
process.stderr.write(
|
|
82
|
+
`Genice enhancement failed for ${scenarioName}: ${message}; writing report without AI section.
|
|
83
|
+
`
|
|
84
|
+
);
|
|
85
|
+
return report.markdown;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// lib/runner/scenarioRunner.ts
|
|
90
|
+
async function resolveIframe(page, selector) {
|
|
91
|
+
const handle = await page.locator(selector).elementHandle();
|
|
92
|
+
if (!handle) throw new Error(`Iframe not found for selector: ${selector}`);
|
|
93
|
+
const frame = await handle.contentFrame();
|
|
94
|
+
if (!frame)
|
|
95
|
+
throw new Error(`Could not get content frame for selector: ${selector}`);
|
|
96
|
+
return frame;
|
|
97
|
+
}
|
|
98
|
+
async function forceGarbageCollection(page) {
|
|
99
|
+
const session = await page.context().newCDPSession(page);
|
|
100
|
+
await session.send("HeapProfiler.enable");
|
|
101
|
+
await session.send("HeapProfiler.collectGarbage");
|
|
102
|
+
await session.send("HeapProfiler.disable");
|
|
103
|
+
await session.detach();
|
|
104
|
+
await page.waitForTimeout(2e3);
|
|
105
|
+
}
|
|
106
|
+
function cleanupSnapshots(paths) {
|
|
107
|
+
if (paths.before && fs.existsSync(paths.before)) fs.unlinkSync(paths.before);
|
|
108
|
+
if (paths.after && fs.existsSync(paths.after)) fs.unlinkSync(paths.after);
|
|
109
|
+
}
|
|
110
|
+
var ScenarioRunner = class {
|
|
111
|
+
constructor(config) {
|
|
112
|
+
this.config = config;
|
|
113
|
+
}
|
|
114
|
+
async launchBrowser() {
|
|
115
|
+
return chromium.launch({ headless: this.config.runner.headless });
|
|
116
|
+
}
|
|
117
|
+
async run(scenario, browser) {
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
const context = await browser.newContext({
|
|
120
|
+
baseURL: this.config.env.baseUrl
|
|
121
|
+
});
|
|
122
|
+
const page = await context.newPage();
|
|
123
|
+
const paths = { before: "", after: "" };
|
|
124
|
+
let report = null;
|
|
125
|
+
let error = null;
|
|
126
|
+
try {
|
|
127
|
+
report = await this.runScenarioInPage(page, scenario, paths);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
130
|
+
} finally {
|
|
131
|
+
await context.close();
|
|
132
|
+
cleanupSnapshots(paths);
|
|
133
|
+
}
|
|
134
|
+
return this.buildResult({ scenario, report, error, startTime });
|
|
135
|
+
}
|
|
136
|
+
async runScenarioInPage(page, scenario, paths) {
|
|
137
|
+
const snapshotsDir = path.join(this.config.runner.outputDir, "snapshots");
|
|
138
|
+
const { profiler, frame } = await this.setupAndProfile(
|
|
139
|
+
page,
|
|
140
|
+
scenario,
|
|
141
|
+
snapshotsDir
|
|
142
|
+
);
|
|
143
|
+
paths.before = await profiler.captureSnapshot("before");
|
|
144
|
+
await this.repeatScenarioActions(scenario, page, frame);
|
|
145
|
+
await forceGarbageCollection(page);
|
|
146
|
+
paths.after = await profiler.captureSnapshot("after");
|
|
147
|
+
const report = await profiler.compare(
|
|
148
|
+
"before",
|
|
149
|
+
"after",
|
|
150
|
+
this.config.runner.topN
|
|
151
|
+
);
|
|
152
|
+
if (report) await this.writeReport(report, scenario);
|
|
153
|
+
return report;
|
|
154
|
+
}
|
|
155
|
+
async setupAndProfile(page, scenario, snapshotsDir) {
|
|
156
|
+
const auth = new AuthManager();
|
|
157
|
+
const pageSetup = new PageSetup();
|
|
158
|
+
await pageSetup.apply(page);
|
|
159
|
+
await auth.login(page, {
|
|
160
|
+
username: this.config.env.userId,
|
|
161
|
+
password: this.config.env.password,
|
|
162
|
+
instanceId: this.config.env.instanceId
|
|
163
|
+
});
|
|
164
|
+
await page.goto(scenario.url());
|
|
165
|
+
const frame = await resolveIframe(page, scenario.microappSelector);
|
|
166
|
+
fs.mkdirSync(snapshotsDir, { recursive: true });
|
|
167
|
+
const profiler = new IframeHeapProfiler(page, frame, snapshotsDir);
|
|
168
|
+
return { profiler, frame };
|
|
169
|
+
}
|
|
170
|
+
async repeatScenarioActions(scenario, page, frame) {
|
|
171
|
+
const repeatCount = scenario.repeat?.() ?? 3;
|
|
172
|
+
for (let i = 0; i < repeatCount; i += 1) {
|
|
173
|
+
await scenario.action(page, frame);
|
|
174
|
+
if (scenario.back) {
|
|
175
|
+
await scenario.back(page);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async writeReport(report, scenario) {
|
|
180
|
+
const reportPath = path.join(
|
|
181
|
+
this.config.runner.outputDir,
|
|
182
|
+
`${scenario.id}.md`
|
|
183
|
+
);
|
|
184
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
185
|
+
const markdown = await enhanceMarkdownIfConfigured(
|
|
186
|
+
report,
|
|
187
|
+
this.config,
|
|
188
|
+
scenario.name
|
|
189
|
+
);
|
|
190
|
+
fs.writeFileSync(reportPath, markdown);
|
|
191
|
+
}
|
|
192
|
+
buildResult(input) {
|
|
193
|
+
const { scenario, report, error, startTime } = input;
|
|
194
|
+
const thresholds = {
|
|
195
|
+
...DEFAULT_THRESHOLDS,
|
|
196
|
+
...scenario.thresholds
|
|
197
|
+
};
|
|
198
|
+
const failureFallback = {
|
|
199
|
+
passed: false,
|
|
200
|
+
reason: error?.message ?? "Snapshot capture failed"
|
|
201
|
+
};
|
|
202
|
+
const thresholdResult = report && !error ? ThresholdEvaluator.evaluate(report, thresholds) : failureFallback;
|
|
203
|
+
return {
|
|
204
|
+
name: scenario.name,
|
|
205
|
+
passed: thresholdResult.passed,
|
|
206
|
+
thresholdResult,
|
|
207
|
+
report,
|
|
208
|
+
durationMs: Date.now() - startTime,
|
|
209
|
+
error
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// lib/runner/batchRunner.ts
|
|
215
|
+
function buildSummary(results) {
|
|
216
|
+
return {
|
|
217
|
+
results,
|
|
218
|
+
totalDurationMs: results.reduce((acc, r) => acc + r.durationMs, 0),
|
|
219
|
+
passCount: results.filter((r) => r.passed).length,
|
|
220
|
+
failCount: results.filter((r) => !r.passed).length
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
var BatchRunner = class {
|
|
224
|
+
constructor(config, registry) {
|
|
225
|
+
this.config = config;
|
|
226
|
+
this.registry = registry;
|
|
227
|
+
this.scenarioRunner = new ScenarioRunner(config);
|
|
228
|
+
}
|
|
229
|
+
scenarioRunner;
|
|
230
|
+
async runAll() {
|
|
231
|
+
const scenarios = this.registry.list().map((entry) => entry.scenario);
|
|
232
|
+
return this.executeScenarios(scenarios);
|
|
233
|
+
}
|
|
234
|
+
async runByTag(tag) {
|
|
235
|
+
const scenarios = this.registry.filterByTag(tag).map((entry) => entry.scenario);
|
|
236
|
+
return this.executeScenarios(scenarios);
|
|
237
|
+
}
|
|
238
|
+
async runByName(key) {
|
|
239
|
+
const scenario = this.registry.get(key);
|
|
240
|
+
if (!scenario) {
|
|
241
|
+
throw new Error(`No scenario registered with key "${key}"`);
|
|
242
|
+
}
|
|
243
|
+
return this.executeScenarios([scenario]);
|
|
244
|
+
}
|
|
245
|
+
async executeScenarios(scenarios) {
|
|
246
|
+
const results = await scenarios.reduce(
|
|
247
|
+
async (acc, scenario) => {
|
|
248
|
+
const prior = await acc;
|
|
249
|
+
const result = await this.runOne(scenario);
|
|
250
|
+
return [...prior, result];
|
|
251
|
+
},
|
|
252
|
+
Promise.resolve([])
|
|
253
|
+
);
|
|
254
|
+
return buildSummary(results);
|
|
255
|
+
}
|
|
256
|
+
async runOne(scenario) {
|
|
257
|
+
const browser = await this.scenarioRunner.launchBrowser();
|
|
258
|
+
try {
|
|
259
|
+
return await this.scenarioRunner.run(scenario, browser);
|
|
260
|
+
} finally {
|
|
261
|
+
await browser.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// lib/reporting/consoleReporter.ts
|
|
267
|
+
var GREEN = "\x1B[32m";
|
|
268
|
+
var RED = "\x1B[31m";
|
|
269
|
+
var BOLD = "\x1B[1m";
|
|
270
|
+
var RESET = "\x1B[0m";
|
|
271
|
+
function formatDuration(ms) {
|
|
272
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
273
|
+
}
|
|
274
|
+
function printLine(text) {
|
|
275
|
+
process.stdout.write(`${text}
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
278
|
+
function printScenario(result) {
|
|
279
|
+
const status = result.passed ? `${GREEN}PASSED${RESET}` : `${RED}FAILED${RESET}`;
|
|
280
|
+
const duration = formatDuration(result.durationMs);
|
|
281
|
+
printLine(` ${status} ${BOLD}${result.name}${RESET} (${duration})`);
|
|
282
|
+
if (!result.passed && result.thresholdResult.reason) {
|
|
283
|
+
printLine(` ${RED}\u2192 ${result.thresholdResult.reason}${RESET}`);
|
|
284
|
+
}
|
|
285
|
+
if (!result.passed && result.error) {
|
|
286
|
+
printLine(` ${RED}\u2192 Error: ${result.error.message}${RESET}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
var ConsoleReporter = class {
|
|
290
|
+
write(summary) {
|
|
291
|
+
printLine("");
|
|
292
|
+
printLine(`${BOLD}MEMORY LEAK RUN RESULTS${RESET}`);
|
|
293
|
+
printLine("\u2500".repeat(50));
|
|
294
|
+
summary.results.forEach(printScenario);
|
|
295
|
+
printLine("\u2500".repeat(50));
|
|
296
|
+
const total = summary.results.length;
|
|
297
|
+
const duration = formatDuration(summary.totalDurationMs);
|
|
298
|
+
printLine(
|
|
299
|
+
`${summary.passCount} passed, ${summary.failCount} failed of ${total} scenarios (${duration})`
|
|
300
|
+
);
|
|
301
|
+
printLine("");
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// lib/reporting/junitReporter.ts
|
|
306
|
+
import fs2 from "node:fs";
|
|
307
|
+
import path2 from "node:path";
|
|
308
|
+
function escapeXml(str) {
|
|
309
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
310
|
+
}
|
|
311
|
+
function formatSeconds(ms) {
|
|
312
|
+
return (ms / 1e3).toFixed(1);
|
|
313
|
+
}
|
|
314
|
+
function renderTestCase(result) {
|
|
315
|
+
const timeAttr = `time="${formatSeconds(result.durationMs)}"`;
|
|
316
|
+
const open = ` <testcase name="${escapeXml(
|
|
317
|
+
result.name
|
|
318
|
+
)}" classname="memory-leak" ${timeAttr}>`;
|
|
319
|
+
if (result.passed) {
|
|
320
|
+
return `${open}
|
|
321
|
+
</testcase>`;
|
|
322
|
+
}
|
|
323
|
+
const message = result.thresholdResult.reason ?? result.error?.message ?? "Unknown failure";
|
|
324
|
+
const body = result.thresholdResult.reason ? `See leak-reports/${result.name}.md for full analysis` : result.error?.stack ?? "";
|
|
325
|
+
return `${open}
|
|
326
|
+
<failure message="${escapeXml(message)}">
|
|
327
|
+
${escapeXml(body)}
|
|
328
|
+
</failure>
|
|
329
|
+
</testcase>`;
|
|
330
|
+
}
|
|
331
|
+
var JunitReporter = class {
|
|
332
|
+
write(summary, outputDir) {
|
|
333
|
+
const totalTime = formatSeconds(summary.totalDurationMs);
|
|
334
|
+
const testCases = summary.results.map(renderTestCase).join("\n");
|
|
335
|
+
const xml = [
|
|
336
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
337
|
+
`<testsuites name="encw-leak-runner" time="${totalTime}">`,
|
|
338
|
+
` <testsuite name="microapp-memory-leaks" tests="${summary.results.length}" failures="${summary.failCount}" time="${totalTime}">`,
|
|
339
|
+
testCases,
|
|
340
|
+
" </testsuite>",
|
|
341
|
+
"</testsuites>"
|
|
342
|
+
].join("\n");
|
|
343
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
344
|
+
fs2.writeFileSync(path2.join(outputDir, "test-results.xml"), xml, "utf8");
|
|
345
|
+
return Promise.resolve();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// lib/config/sources/cliOverrideConfigSource.ts
|
|
350
|
+
var CliOverrideConfigSource = class {
|
|
351
|
+
constructor(overrides) {
|
|
352
|
+
this.overrides = overrides;
|
|
353
|
+
}
|
|
354
|
+
priority = 3;
|
|
355
|
+
name = "cli";
|
|
356
|
+
load() {
|
|
357
|
+
const runner = {};
|
|
358
|
+
if (this.overrides.headless !== void 0) {
|
|
359
|
+
runner.headless = this.overrides.headless;
|
|
360
|
+
}
|
|
361
|
+
if (this.overrides.outputDir !== void 0) {
|
|
362
|
+
runner.outputDir = this.overrides.outputDir;
|
|
363
|
+
}
|
|
364
|
+
if (this.overrides.topN !== void 0) {
|
|
365
|
+
runner.topN = this.overrides.topN;
|
|
366
|
+
}
|
|
367
|
+
return Object.keys(runner).length === 0 ? {} : { runner };
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// lib/config/sources/envVarConfigSource.ts
|
|
372
|
+
var EnvVarConfigSource = class {
|
|
373
|
+
priority = 2;
|
|
374
|
+
name = "env";
|
|
375
|
+
load() {
|
|
376
|
+
const runner = {};
|
|
377
|
+
if (process.env.HEADLESS !== void 0) {
|
|
378
|
+
runner.headless = process.env.HEADLESS !== "false";
|
|
379
|
+
}
|
|
380
|
+
if (process.env.LEAK_OUTPUT_DIR) {
|
|
381
|
+
runner.outputDir = process.env.LEAK_OUTPUT_DIR;
|
|
382
|
+
}
|
|
383
|
+
if (process.env.LEAK_TOP_N) {
|
|
384
|
+
const parsed = parseInt(process.env.LEAK_TOP_N, 10);
|
|
385
|
+
if (Number.isFinite(parsed) && parsed > 0) runner.topN = parsed;
|
|
386
|
+
}
|
|
387
|
+
return Object.keys(runner).length === 0 ? {} : { runner };
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// lib/config/sources/fileConfigSource.ts
|
|
392
|
+
import fs3 from "node:fs";
|
|
393
|
+
|
|
394
|
+
// lib/config/runnerConfigSchema.ts
|
|
395
|
+
import { z } from "zod";
|
|
396
|
+
var runnerOptionsSchema = z.object({
|
|
397
|
+
headless: z.boolean(),
|
|
398
|
+
outputDir: z.string().min(1),
|
|
399
|
+
topN: z.number().int().min(1)
|
|
400
|
+
}).partial();
|
|
401
|
+
var aiConfigFileSchema = z.object({
|
|
402
|
+
enabled: z.boolean(),
|
|
403
|
+
model: z.string().min(1),
|
|
404
|
+
temperature: z.number().min(0).max(2)
|
|
405
|
+
}).partial();
|
|
406
|
+
var runnerConfigFileSchema = z.object({
|
|
407
|
+
runner: runnerOptionsSchema,
|
|
408
|
+
ai: aiConfigFileSchema
|
|
409
|
+
}).partial().strict();
|
|
410
|
+
|
|
411
|
+
// lib/config/sources/fileConfigSource.ts
|
|
412
|
+
var FileConfigSource = class {
|
|
413
|
+
constructor(filePath) {
|
|
414
|
+
this.filePath = filePath;
|
|
415
|
+
}
|
|
416
|
+
priority = 1;
|
|
417
|
+
name = "file";
|
|
418
|
+
load() {
|
|
419
|
+
if (!fs3.existsSync(this.filePath)) return {};
|
|
420
|
+
const raw = fs3.readFileSync(this.filePath, "utf8");
|
|
421
|
+
let parsed;
|
|
422
|
+
try {
|
|
423
|
+
parsed = JSON.parse(raw);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Failed to parse config file ${this.filePath}: ${err.message}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const result = runnerConfigFileSchema.safeParse(parsed);
|
|
430
|
+
if (!result.success) {
|
|
431
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
432
|
+
throw new Error(`Invalid config file ${this.filePath}:
|
|
433
|
+
${issues}`);
|
|
434
|
+
}
|
|
435
|
+
return result.data;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// lib/config/sources/configSource.ts
|
|
440
|
+
var BUILT_IN_DEFAULTS = {
|
|
441
|
+
runner: {
|
|
442
|
+
headless: true,
|
|
443
|
+
outputDir: "./leak-reports/",
|
|
444
|
+
topN: 5
|
|
445
|
+
},
|
|
446
|
+
ai: {
|
|
447
|
+
enabled: false,
|
|
448
|
+
model: "Claude3.7",
|
|
449
|
+
temperature: 0.3
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// lib/config/runnerConfigLoader.ts
|
|
454
|
+
function applyRunnerPayload(acc, payload) {
|
|
455
|
+
return {
|
|
456
|
+
headless: payload.headless ?? acc.headless,
|
|
457
|
+
outputDir: payload.outputDir ?? acc.outputDir,
|
|
458
|
+
topN: payload.topN ?? acc.topN
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function applyAiPayload(acc, payload) {
|
|
462
|
+
return {
|
|
463
|
+
aiEnabled: payload.enabled ?? acc.aiEnabled,
|
|
464
|
+
aiModel: payload.model ?? acc.aiModel,
|
|
465
|
+
aiTemperature: payload.temperature ?? acc.aiTemperature
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function applySource(acc, source) {
|
|
469
|
+
const payload = source.load();
|
|
470
|
+
return {
|
|
471
|
+
runner: payload.runner ? applyRunnerPayload(acc.runner, payload.runner) : acc.runner,
|
|
472
|
+
ai: payload.ai ? applyAiPayload(acc.ai, payload.ai) : acc.ai
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
var RunnerConfigLoader = class {
|
|
476
|
+
constructor(sources) {
|
|
477
|
+
this.sources = sources;
|
|
478
|
+
}
|
|
479
|
+
resolveOptions() {
|
|
480
|
+
const ordered = [...this.sources].sort((a, b) => a.priority - b.priority);
|
|
481
|
+
const initial = {
|
|
482
|
+
runner: {
|
|
483
|
+
headless: BUILT_IN_DEFAULTS.runner.headless ?? true,
|
|
484
|
+
outputDir: BUILT_IN_DEFAULTS.runner.outputDir ?? "./leak-reports/",
|
|
485
|
+
topN: BUILT_IN_DEFAULTS.runner.topN ?? 5
|
|
486
|
+
},
|
|
487
|
+
ai: {
|
|
488
|
+
aiEnabled: BUILT_IN_DEFAULTS.ai.enabled ?? false,
|
|
489
|
+
aiModel: BUILT_IN_DEFAULTS.ai.model ?? "Claude3.7",
|
|
490
|
+
aiTemperature: BUILT_IN_DEFAULTS.ai.temperature ?? 0.3
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
const resolved = ordered.reduce(applySource, initial);
|
|
494
|
+
return {
|
|
495
|
+
runner: resolved.runner,
|
|
496
|
+
aiEnabled: resolved.ai.aiEnabled,
|
|
497
|
+
aiModel: resolved.ai.aiModel,
|
|
498
|
+
aiTemperature: resolved.ai.aiTemperature
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// lib/config/missingRequiredParamError.ts
|
|
504
|
+
var MissingRequiredParamError = class extends Error {
|
|
505
|
+
constructor(missing) {
|
|
506
|
+
super(
|
|
507
|
+
`Missing required parameter(s): ${missing.join(", ")}.
|
|
508
|
+
Provide via CLI flag (--base-url / --instance-id / --user-id) or env var (BASE_URL / ENCW_INSTANCE_ID / ENCW_USER_ID / ENCW_PASSWORD).`
|
|
509
|
+
);
|
|
510
|
+
this.missing = missing;
|
|
511
|
+
this.name = "MissingRequiredParamError";
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// lib/config/requiredEnvParams.ts
|
|
516
|
+
var RequiredEnvParamsResolver = class {
|
|
517
|
+
resolve(cliOpts) {
|
|
518
|
+
const baseUrl = this.resolveBaseUrl(cliOpts);
|
|
519
|
+
const instanceId = this.resolveInstanceId(cliOpts);
|
|
520
|
+
const userId = this.resolveUserId(cliOpts);
|
|
521
|
+
const password = this.resolvePassword();
|
|
522
|
+
const params = { baseUrl, instanceId, userId, password };
|
|
523
|
+
const missing = this.collectMissing(params);
|
|
524
|
+
if (missing.length > 0) throw new MissingRequiredParamError(missing);
|
|
525
|
+
return params;
|
|
526
|
+
}
|
|
527
|
+
resolveBaseUrl(cliOpts) {
|
|
528
|
+
return cliOpts.baseUrl || process.env.BASE_URL || "";
|
|
529
|
+
}
|
|
530
|
+
resolveInstanceId(cliOpts) {
|
|
531
|
+
return cliOpts.instanceId || process.env.ENCW_INSTANCE_ID || "";
|
|
532
|
+
}
|
|
533
|
+
resolveUserId(cliOpts) {
|
|
534
|
+
return cliOpts.userId || process.env.ENCW_USER_ID || "";
|
|
535
|
+
}
|
|
536
|
+
resolvePassword() {
|
|
537
|
+
return process.env.ENCW_PASSWORD || "";
|
|
538
|
+
}
|
|
539
|
+
collectMissing(params) {
|
|
540
|
+
const missing = [];
|
|
541
|
+
if (!params.baseUrl) missing.push("baseUrl");
|
|
542
|
+
if (!params.instanceId) missing.push("instanceId");
|
|
543
|
+
if (!params.userId) missing.push("userId");
|
|
544
|
+
if (!params.password) missing.push("password (ENCW_PASSWORD)");
|
|
545
|
+
return missing;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// lib/cli/commands/runCommand.ts
|
|
550
|
+
var RunCommand = class {
|
|
551
|
+
register(program, deps) {
|
|
552
|
+
program.command("run [key]").description(
|
|
553
|
+
"Run a scenario by `<microapp>/<id>`, or use --all / --tag to run multiple"
|
|
554
|
+
).option("--all", "Run all registered scenarios").option("--tag <tag>", "Run all scenarios matching a tag").option("--base-url <url>", "Override base URL").option("--instance-id <id>", "Override instance ID").option("--user-id <id>", "Override user ID").option("--output-dir <dir>", "Override output directory").option(
|
|
555
|
+
"--top-n <n>",
|
|
556
|
+
"Override top-N for heap comparison",
|
|
557
|
+
(v) => parseInt(v, 10)
|
|
558
|
+
).option(
|
|
559
|
+
"--headless <bool>",
|
|
560
|
+
"Override headless mode",
|
|
561
|
+
(v) => v !== "false"
|
|
562
|
+
).option("--config-file <path>", "Path to leak-runner.config.json").action(async (key, options) => {
|
|
563
|
+
try {
|
|
564
|
+
const config = this.buildConfig(options, deps);
|
|
565
|
+
const runner = new BatchRunner(config, deps.registry);
|
|
566
|
+
const summary = await this.selectSummary(runner, key, options);
|
|
567
|
+
new ConsoleReporter().write(summary);
|
|
568
|
+
await new JunitReporter().write(summary, config.runner.outputDir);
|
|
569
|
+
process.exit(summary.failCount > 0 ? 1 : 0);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (err instanceof MissingRequiredParamError) {
|
|
572
|
+
process.stderr.write(`Configuration error: ${err.message}
|
|
573
|
+
`);
|
|
574
|
+
process.exit(2);
|
|
575
|
+
}
|
|
576
|
+
process.stderr.write(`Fatal: ${err.message}
|
|
577
|
+
`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
buildConfig(options, deps) {
|
|
583
|
+
const filePath = options.configFile ?? `${process.cwd()}/leak-runner.config.json`;
|
|
584
|
+
const loader = new RunnerConfigLoader([
|
|
585
|
+
new FileConfigSource(filePath),
|
|
586
|
+
new EnvVarConfigSource(),
|
|
587
|
+
new CliOverrideConfigSource({
|
|
588
|
+
headless: options.headless,
|
|
589
|
+
outputDir: options.outputDir,
|
|
590
|
+
topN: options.topN
|
|
591
|
+
})
|
|
592
|
+
]);
|
|
593
|
+
const resolved = loader.resolveOptions();
|
|
594
|
+
const env = deps.envParams.resolve({
|
|
595
|
+
baseUrl: options.baseUrl,
|
|
596
|
+
instanceId: options.instanceId,
|
|
597
|
+
userId: options.userId
|
|
598
|
+
});
|
|
599
|
+
let ai;
|
|
600
|
+
if (resolved.aiEnabled) {
|
|
601
|
+
ai = {
|
|
602
|
+
model: resolved.aiModel,
|
|
603
|
+
temperature: resolved.aiTemperature,
|
|
604
|
+
apiKey: process.env.GENICE_API_KEY ?? ""
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return { env, runner: resolved.runner, ai };
|
|
608
|
+
}
|
|
609
|
+
async selectSummary(runner, key, options) {
|
|
610
|
+
if (options.all) return runner.runAll();
|
|
611
|
+
if (options.tag) {
|
|
612
|
+
const summary = await runner.runByTag(options.tag);
|
|
613
|
+
if (summary.results.length === 0) {
|
|
614
|
+
process.stderr.write(
|
|
615
|
+
`Warning: no scenarios matched tag "${options.tag}"
|
|
616
|
+
`
|
|
617
|
+
);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
return summary;
|
|
621
|
+
}
|
|
622
|
+
if (key) return runner.runByName(key);
|
|
623
|
+
process.stderr.write(
|
|
624
|
+
"Error: specify a scenario key (e.g. one-admin/export-navigation), --all, or --tag <tag>\n"
|
|
625
|
+
);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
throw new Error("unreachable");
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// lib/cli/commands/listCommand.ts
|
|
632
|
+
var ListCommand = class {
|
|
633
|
+
register(program, deps) {
|
|
634
|
+
program.command("list").description("List all registered scenarios").option("--tag <tag>", "Filter by tag").action((options) => {
|
|
635
|
+
const entries = options.tag ? deps.registry.filterByTag(options.tag) : deps.registry.list();
|
|
636
|
+
if (entries.length === 0) {
|
|
637
|
+
process.stdout.write("No scenarios found.\n");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const keyWidth = Math.max(...entries.map((e) => e.key.length), 32);
|
|
641
|
+
process.stdout.write(`
|
|
642
|
+
REGISTERED SCENARIOS (${entries.length})
|
|
643
|
+
|
|
644
|
+
`);
|
|
645
|
+
entries.forEach(({ key, scenario }) => {
|
|
646
|
+
const tags = scenario.tags?.length ? `[${scenario.tags.join(", ")}]` : "";
|
|
647
|
+
process.stdout.write(
|
|
648
|
+
` ${key.padEnd(keyWidth)} ${tags.padEnd(20)} ${scenario.description}
|
|
649
|
+
`
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
process.stdout.write("\n");
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// lib/registry/scenarioRegistry.ts
|
|
658
|
+
var ScenarioRegistry = class {
|
|
659
|
+
entries = /* @__PURE__ */ new Map();
|
|
660
|
+
register(group) {
|
|
661
|
+
group.scenarios.forEach((scenario) => {
|
|
662
|
+
const key = `${group.microapp}/${scenario.id}`;
|
|
663
|
+
if (this.entries.has(key)) {
|
|
664
|
+
throw new Error(`Duplicate scenario: ${key}`);
|
|
665
|
+
}
|
|
666
|
+
this.entries.set(key, scenario);
|
|
667
|
+
});
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
get(key) {
|
|
671
|
+
return this.entries.get(key);
|
|
672
|
+
}
|
|
673
|
+
has(key) {
|
|
674
|
+
return this.entries.has(key);
|
|
675
|
+
}
|
|
676
|
+
size() {
|
|
677
|
+
return this.entries.size;
|
|
678
|
+
}
|
|
679
|
+
list() {
|
|
680
|
+
return Array.from(this.entries.entries()).map(([key, scenario]) => ({ key, scenario })).sort((a, b) => a.key.localeCompare(b.key));
|
|
681
|
+
}
|
|
682
|
+
filterByTag(tag) {
|
|
683
|
+
return this.list().filter((entry) => entry.scenario.tags?.includes(tag));
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// lib/scenarios/one-admin/page-models/SelectSettingsPageModel.ts
|
|
688
|
+
var SelectSettingsPageModel = class {
|
|
689
|
+
constructor(frame) {
|
|
690
|
+
this.frame = frame;
|
|
691
|
+
}
|
|
692
|
+
get container() {
|
|
693
|
+
return this.frame.locator("#one-admin-console-container");
|
|
694
|
+
}
|
|
695
|
+
get exportButton() {
|
|
696
|
+
return this.frame.getByRole("button", { name: "Export" });
|
|
697
|
+
}
|
|
698
|
+
async expandTreeItem(name) {
|
|
699
|
+
const treeItem = this.frame.locator(
|
|
700
|
+
`[role="treeitem"]:has-text("${name}")`
|
|
701
|
+
);
|
|
702
|
+
await treeItem.locator('button[aria-label*="expand"], button[aria-label*="toggle"]').click();
|
|
703
|
+
}
|
|
704
|
+
async selectTreeItem(name) {
|
|
705
|
+
await this.frame.locator(`[role="treeitem"]:has-text("${name}")`).click();
|
|
706
|
+
}
|
|
707
|
+
async clickExport() {
|
|
708
|
+
await this.exportButton.click();
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// lib/scenarios/one-admin/page-models/ExportPageModel.ts
|
|
713
|
+
var IFRAME_SELECTOR = "iframe#pui-iframe-container-emAdminUI";
|
|
714
|
+
var SETTINGS_URL_PATTERN = /\/admin\/oneadmin\/migrate(?:\/)?$/;
|
|
715
|
+
var ExportPageModel = class {
|
|
716
|
+
constructor(frame) {
|
|
717
|
+
this.frame = frame;
|
|
718
|
+
}
|
|
719
|
+
get sourceDataTab() {
|
|
720
|
+
return this.frame.getByRole("tab", { name: /Source Data/i });
|
|
721
|
+
}
|
|
722
|
+
get dataTable() {
|
|
723
|
+
return this.frame.locator('[role="table"]').first();
|
|
724
|
+
}
|
|
725
|
+
static async clickBackAndWaitForSettings(page) {
|
|
726
|
+
await Promise.all([
|
|
727
|
+
page.waitForURL(SETTINGS_URL_PATTERN),
|
|
728
|
+
page.frameLocator(IFRAME_SELECTOR).getByRole("button", { name: /back/i }).click()
|
|
729
|
+
]);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// lib/scenarios/one-admin/export-navigation.scenario.ts
|
|
734
|
+
var exportNavigationScenario = {
|
|
735
|
+
id: "export-navigation",
|
|
736
|
+
name: "Export Navigation",
|
|
737
|
+
description: "Navigate to export page and come back - verify iframe GC",
|
|
738
|
+
tags: ["critical"],
|
|
739
|
+
microappSelector: "iframe#pui-iframe-container-emAdminUI",
|
|
740
|
+
url: () => "/admin/oneadmin/migrate",
|
|
741
|
+
async action(page, frame) {
|
|
742
|
+
const settings = new SelectSettingsPageModel(frame);
|
|
743
|
+
await settings.container.waitFor({ state: "visible" });
|
|
744
|
+
await settings.expandTreeItem("eFolder");
|
|
745
|
+
await settings.selectTreeItem("Enhanced Conditions");
|
|
746
|
+
await settings.clickExport();
|
|
747
|
+
const exportPage = new ExportPageModel(frame);
|
|
748
|
+
await exportPage.sourceDataTab.waitFor({ state: "visible" });
|
|
749
|
+
await exportPage.dataTable.waitFor({ state: "visible" });
|
|
750
|
+
},
|
|
751
|
+
async back(page) {
|
|
752
|
+
await ExportPageModel.clickBackAndWaitForSettings(page);
|
|
753
|
+
},
|
|
754
|
+
repeat: () => 3,
|
|
755
|
+
thresholds: {
|
|
756
|
+
maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
|
|
757
|
+
maxNewLeakGroups: 0
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// lib/scenarios/one-admin/index.ts
|
|
762
|
+
var oneAdminScenarios = [
|
|
763
|
+
exportNavigationScenario
|
|
764
|
+
];
|
|
765
|
+
|
|
766
|
+
// lib/scenarios/index.ts
|
|
767
|
+
var scenarioRegistry = new ScenarioRegistry().register({
|
|
768
|
+
microapp: "one-admin",
|
|
769
|
+
scenarios: oneAdminScenarios
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// lib/cli/index.ts
|
|
773
|
+
function buildProgram(deps = defaultDeps()) {
|
|
774
|
+
const program = new Command();
|
|
775
|
+
program.name("leak-runner").description("Microapp memory leak runner for Encompass Web").version("1.0.0");
|
|
776
|
+
const commands = [new RunCommand(), new ListCommand()];
|
|
777
|
+
commands.forEach((cmd) => cmd.register(program, deps));
|
|
778
|
+
return program;
|
|
779
|
+
}
|
|
780
|
+
function defaultDeps() {
|
|
781
|
+
return {
|
|
782
|
+
registry: scenarioRegistry,
|
|
783
|
+
envParams: new RequiredEnvParamsResolver()
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// bin/leak-runner.ts
|
|
788
|
+
buildProgram().parseAsync(process.argv).catch((err) => {
|
|
789
|
+
process.stderr.write(`Fatal: ${err.message ?? String(err)}
|
|
790
|
+
`);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
});
|