@elliemae/encw-leak-runner 1.0.2 → 1.0.4

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 (135) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +34 -17
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/bin/leak-runner.js +80 -9
  5. package/dist/cjs/cli/commands/runCommand.js +7 -2
  6. package/dist/cjs/config/requiredEnvParams.js +14 -2
  7. package/dist/cjs/config/runnerConfigLoader.js +6 -3
  8. package/dist/cjs/config/runnerConfigSchema.js +5 -1
  9. package/dist/cjs/config/unknownEnvError.js +36 -0
  10. package/dist/cjs/index.js +1 -0
  11. package/dist/cjs/runner/scenarioRunner.js +9 -1
  12. package/dist/cjs/scenarios/one-admin/export-navigation.scenario.js +10 -1
  13. package/dist/cjs/scenarios/one-admin/page-models/AdminLandingPageModel.js +42 -0
  14. package/dist/cjs/scenarios/one-admin/page-models/index.js +2 -0
  15. package/dist/esm/cli/commands/runCommand.js +11 -3
  16. package/dist/esm/config/requiredEnvParams.js +14 -2
  17. package/dist/esm/config/runnerConfigLoader.js +6 -3
  18. package/dist/esm/config/runnerConfigSchema.js +5 -1
  19. package/dist/esm/config/unknownEnvError.js +16 -0
  20. package/dist/esm/index.js +3 -1
  21. package/dist/esm/runner/scenarioRunner.js +9 -1
  22. package/dist/esm/scenarios/one-admin/export-navigation.scenario.js +11 -1
  23. package/dist/esm/scenarios/one-admin/page-models/AdminLandingPageModel.js +22 -0
  24. package/dist/esm/scenarios/one-admin/page-models/index.js +2 -0
  25. package/dist/types/lib/config/requiredEnvParams.d.ts +4 -1
  26. package/dist/types/lib/config/runnerConfigLoader.d.ts +1 -0
  27. package/dist/types/lib/config/runnerConfigSchema.d.ts +7 -0
  28. package/dist/types/lib/config/unknownEnvError.d.ts +5 -0
  29. package/dist/types/lib/index.d.ts +1 -1
  30. package/dist/types/lib/runner/aiEnhancementStep.d.ts +4 -3
  31. package/dist/types/lib/scenarios/one-admin/page-models/AdminLandingPageModel.d.ts +8 -0
  32. package/dist/types/lib/scenarios/one-admin/page-models/index.d.ts +1 -0
  33. package/dist/types/lib/types/scenario.d.ts +1 -0
  34. package/leak-runner.config.json +18 -0
  35. package/leak-runner.schema.json +5 -0
  36. package/lib/cli/commands/runCommand.ts +15 -2
  37. package/lib/config/requiredEnvParams.ts +15 -2
  38. package/lib/config/runnerConfigLoader.ts +12 -5
  39. package/lib/config/runnerConfigSchema.ts +4 -0
  40. package/lib/config/tests/fileConfigSource.test.ts +26 -0
  41. package/lib/config/tests/requiredEnvParams.test.ts +90 -2
  42. package/lib/config/tests/runnerConfigLoader.test.ts +30 -0
  43. package/lib/config/unknownEnvError.ts +13 -0
  44. package/lib/index.ts +1 -0
  45. package/lib/runner/aiEnhancementStep.ts +4 -3
  46. package/lib/runner/scenarioRunner.ts +8 -1
  47. package/lib/scenarios/one-admin/export-navigation.scenario.ts +13 -1
  48. package/lib/scenarios/one-admin/page-models/AdminLandingPageModel.ts +24 -0
  49. package/lib/scenarios/one-admin/page-models/index.ts +1 -0
  50. package/lib/types/scenario.ts +1 -0
  51. package/package.json +3 -3
  52. package/reports/analysis/index.html +1 -1
  53. package/reports/analysis/thresholdEvaluator.ts.html +1 -1
  54. package/reports/browser/iframeHeapProfiler.ts.html +1 -1
  55. package/reports/browser/index.html +1 -1
  56. package/reports/cli/commands/index.html +3 -3
  57. package/reports/cli/commands/listCommand.ts.html +1 -1
  58. package/reports/cli/commands/runCommand.ts.html +44 -5
  59. package/reports/cli/index.html +1 -1
  60. package/reports/cli/index.ts.html +1 -1
  61. package/reports/config/index.html +36 -21
  62. package/reports/config/missingRequiredParamError.ts.html +4 -4
  63. package/reports/config/requiredEnvParams.ts.html +67 -28
  64. package/reports/config/runnerConfigLoader.ts.html +38 -17
  65. package/reports/config/runnerConfigSchema.ts.html +18 -6
  66. package/reports/config/sources/cliOverrideConfigSource.ts.html +1 -1
  67. package/reports/config/sources/configSource.ts.html +2 -2
  68. package/reports/config/sources/envVarConfigSource.ts.html +1 -1
  69. package/reports/config/sources/fileConfigSource.ts.html +13 -13
  70. package/reports/config/sources/index.html +1 -1
  71. package/reports/config/unknownEnvError.ts.html +124 -0
  72. package/reports/index.html +35 -35
  73. package/reports/lcov-report/analysis/index.html +1 -1
  74. package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +1 -1
  75. package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +1 -1
  76. package/reports/lcov-report/browser/index.html +1 -1
  77. package/reports/lcov-report/cli/commands/index.html +3 -3
  78. package/reports/lcov-report/cli/commands/listCommand.ts.html +1 -1
  79. package/reports/lcov-report/cli/commands/runCommand.ts.html +44 -5
  80. package/reports/lcov-report/cli/index.html +1 -1
  81. package/reports/lcov-report/cli/index.ts.html +1 -1
  82. package/reports/lcov-report/config/index.html +36 -21
  83. package/reports/lcov-report/config/missingRequiredParamError.ts.html +4 -4
  84. package/reports/lcov-report/config/requiredEnvParams.ts.html +67 -28
  85. package/reports/lcov-report/config/runnerConfigLoader.ts.html +38 -17
  86. package/reports/lcov-report/config/runnerConfigSchema.ts.html +18 -6
  87. package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +1 -1
  88. package/reports/lcov-report/config/sources/configSource.ts.html +2 -2
  89. package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +1 -1
  90. package/reports/lcov-report/config/sources/fileConfigSource.ts.html +13 -13
  91. package/reports/lcov-report/config/sources/index.html +1 -1
  92. package/reports/lcov-report/config/unknownEnvError.ts.html +124 -0
  93. package/reports/lcov-report/index.html +35 -35
  94. package/reports/lcov-report/registry/index.html +1 -1
  95. package/reports/lcov-report/registry/scenarioRegistry.ts.html +1 -1
  96. package/reports/lcov-report/reporting/consoleReporter.ts.html +1 -1
  97. package/reports/lcov-report/reporting/index.html +1 -1
  98. package/reports/lcov-report/reporting/junitReporter.ts.html +1 -1
  99. package/reports/lcov-report/runner/aiEnhancementStep.ts.html +8 -5
  100. package/reports/lcov-report/runner/batchRunner.ts.html +1 -1
  101. package/reports/lcov-report/runner/index.html +15 -15
  102. package/reports/lcov-report/runner/scenarioRunner.ts.html +32 -11
  103. package/reports/lcov-report/scenarios/index.html +1 -1
  104. package/reports/lcov-report/scenarios/index.ts.html +1 -1
  105. package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +44 -8
  106. package/reports/lcov-report/scenarios/one-admin/index.html +11 -11
  107. package/reports/lcov-report/scenarios/one-admin/index.ts.html +1 -1
  108. package/reports/lcov-report/scenarios/one-admin/page-models/AdminLandingPageModel.ts.html +157 -0
  109. package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +1 -1
  110. package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +1 -1
  111. package/reports/lcov-report/scenarios/one-admin/page-models/index.html +19 -4
  112. package/reports/lcov-report/types/config.ts.html +1 -1
  113. package/reports/lcov-report/types/index.html +1 -1
  114. package/reports/lcov.info +302 -230
  115. package/reports/registry/index.html +1 -1
  116. package/reports/registry/scenarioRegistry.ts.html +1 -1
  117. package/reports/reporting/consoleReporter.ts.html +1 -1
  118. package/reports/reporting/index.html +1 -1
  119. package/reports/reporting/junitReporter.ts.html +1 -1
  120. package/reports/runner/aiEnhancementStep.ts.html +8 -5
  121. package/reports/runner/batchRunner.ts.html +1 -1
  122. package/reports/runner/index.html +15 -15
  123. package/reports/runner/scenarioRunner.ts.html +32 -11
  124. package/reports/scenarios/index.html +1 -1
  125. package/reports/scenarios/index.ts.html +1 -1
  126. package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +44 -8
  127. package/reports/scenarios/one-admin/index.html +11 -11
  128. package/reports/scenarios/one-admin/index.ts.html +1 -1
  129. package/reports/scenarios/one-admin/page-models/AdminLandingPageModel.ts.html +157 -0
  130. package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +1 -1
  131. package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +1 -1
  132. package/reports/scenarios/one-admin/page-models/index.html +19 -4
  133. package/reports/types/config.ts.html +1 -1
  134. package/reports/types/index.html +1 -1
  135. package/test-report.xml +75 -62
@@ -0,0 +1,22 @@
1
+ class AdminLandingPageModel {
2
+ constructor(frame) {
3
+ this.frame = frame;
4
+ }
5
+ frame;
6
+ get oneAdminConsoleOption() {
7
+ return this.frame.getByText("ONE ADMIN CONSOLE");
8
+ }
9
+ async clickOneAdminConsole() {
10
+ await this.oneAdminConsoleOption.click();
11
+ }
12
+ static async acceptCookiesIfShown(page) {
13
+ try {
14
+ await page.locator("#onetrust-group-container").waitFor({ state: "visible", timeout: 5e3 });
15
+ await page.locator("button", { hasText: "Accept All Cookies" }).click();
16
+ } catch {
17
+ }
18
+ }
19
+ }
20
+ export {
21
+ AdminLandingPageModel
22
+ };
@@ -1,6 +1,8 @@
1
+ import { AdminLandingPageModel } from "./AdminLandingPageModel.js";
1
2
  import { SelectSettingsPageModel } from "./SelectSettingsPageModel.js";
2
3
  import { ExportPageModel } from "./ExportPageModel.js";
3
4
  export {
5
+ AdminLandingPageModel,
4
6
  ExportPageModel,
5
7
  SelectSettingsPageModel
6
8
  };
@@ -1,11 +1,14 @@
1
1
  import type { EnvironmentParams } from '../types/config.js';
2
2
  import { MissingRequiredParamError } from './missingRequiredParamError.js';
3
+ import { UnknownEnvError } from './unknownEnvError.js';
3
4
  export interface CliEnvOpts {
4
5
  readonly baseUrl?: string;
6
+ readonly env?: string;
7
+ readonly envs?: Readonly<Record<string, string>>;
5
8
  readonly instanceId?: string;
6
9
  readonly userId?: string;
7
10
  }
8
- export { MissingRequiredParamError };
11
+ export { MissingRequiredParamError, UnknownEnvError };
9
12
  export declare class RequiredEnvParamsResolver {
10
13
  resolve(cliOpts: CliEnvOpts): EnvironmentParams;
11
14
  private resolveBaseUrl;
@@ -5,6 +5,7 @@ export interface ResolvedRunnerConfigPartial {
5
5
  readonly aiEnabled: boolean;
6
6
  readonly aiModel: string;
7
7
  readonly aiTemperature: number;
8
+ readonly envs: Readonly<Record<string, string>>;
8
9
  }
9
10
  export declare class RunnerConfigLoader {
10
11
  private readonly sources;
@@ -25,7 +25,9 @@ export declare const aiConfigFileSchema: z.ZodObject<{
25
25
  model?: string | undefined;
26
26
  temperature?: number | undefined;
27
27
  }>;
28
+ export declare const envsSchema: z.ZodRecord<z.ZodString, z.ZodString>;
28
29
  export declare const runnerConfigFileSchema: z.ZodObject<{
30
+ $schema: z.ZodOptional<z.ZodOptional<z.ZodString>>;
29
31
  runner: z.ZodOptional<z.ZodObject<{
30
32
  headless: z.ZodOptional<z.ZodBoolean>;
31
33
  outputDir: z.ZodOptional<z.ZodString>;
@@ -52,7 +54,9 @@ export declare const runnerConfigFileSchema: z.ZodObject<{
52
54
  model?: string | undefined;
53
55
  temperature?: number | undefined;
54
56
  }>>;
57
+ envs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
55
58
  }, "strict", z.ZodTypeAny, {
59
+ $schema?: string | undefined;
56
60
  runner?: {
57
61
  headless?: boolean | undefined;
58
62
  outputDir?: string | undefined;
@@ -63,7 +67,9 @@ export declare const runnerConfigFileSchema: z.ZodObject<{
63
67
  model?: string | undefined;
64
68
  temperature?: number | undefined;
65
69
  } | undefined;
70
+ envs?: Record<string, string> | undefined;
66
71
  }, {
72
+ $schema?: string | undefined;
67
73
  runner?: {
68
74
  headless?: boolean | undefined;
69
75
  outputDir?: string | undefined;
@@ -74,5 +80,6 @@ export declare const runnerConfigFileSchema: z.ZodObject<{
74
80
  model?: string | undefined;
75
81
  temperature?: number | undefined;
76
82
  } | undefined;
83
+ envs?: Record<string, string> | undefined;
77
84
  }>;
78
85
  export type RawRunnerConfigFile = z.infer<typeof runnerConfigFileSchema>;
@@ -0,0 +1,5 @@
1
+ export declare class UnknownEnvError extends Error {
2
+ readonly name: string;
3
+ readonly knownEnvs: readonly string[];
4
+ constructor(name: string, knownEnvs: readonly string[]);
5
+ }
@@ -14,5 +14,5 @@ export { RunnerConfigLoader } from './config/runnerConfigLoader.js';
14
14
  export { FileConfigSource } from './config/sources/fileConfigSource.js';
15
15
  export { EnvVarConfigSource } from './config/sources/envVarConfigSource.js';
16
16
  export { CliOverrideConfigSource } from './config/sources/cliOverrideConfigSource.js';
17
- export { RequiredEnvParamsResolver, MissingRequiredParamError, } from './config/requiredEnvParams.js';
17
+ export { RequiredEnvParamsResolver, MissingRequiredParamError, UnknownEnvError, } from './config/requiredEnvParams.js';
18
18
  export { buildProgram, defaultDeps } from './cli/index.js';
@@ -8,8 +8,9 @@ import type { RunnerConfig } from '../types/config.js';
8
8
  *
9
9
  * If the config carries no AI config, return the report's original markdown
10
10
  * untouched (no AI module loaded).
11
- * @param report
12
- * @param config
13
- * @param scenarioName
11
+ * @param {ComparisonReport} report - Heap-doctor comparison report to enhance.
12
+ * @param {RunnerConfig} config - Runner config; only `config.ai` is consulted.
13
+ * @param {string} scenarioName - Scenario name used in stderr warnings.
14
+ * @returns {Promise<string>} Markdown for the report, with or without AI section.
14
15
  */
15
16
  export declare function enhanceMarkdownIfConfigured(report: ComparisonReport, config: RunnerConfig, scenarioName: string): Promise<string>;
@@ -0,0 +1,8 @@
1
+ import type { Frame, Locator, Page } from '@playwright/test';
2
+ export declare class AdminLandingPageModel {
3
+ private readonly frame;
4
+ constructor(frame: Frame);
5
+ get oneAdminConsoleOption(): Locator;
6
+ clickOneAdminConsole(): Promise<void>;
7
+ static acceptCookiesIfShown(page: Page): Promise<void>;
8
+ }
@@ -1,2 +1,3 @@
1
+ export { AdminLandingPageModel } from './AdminLandingPageModel.js';
1
2
  export { SelectSettingsPageModel } from './SelectSettingsPageModel.js';
2
3
  export { ExportPageModel } from './ExportPageModel.js';
@@ -7,6 +7,7 @@ export interface MicroappLeakScenario {
7
7
  tags?: string[];
8
8
  microappSelector: string;
9
9
  url(): string;
10
+ setup?(page: Page, frame: Frame): Promise<void>;
10
11
  action(page: Page, frame: Frame): Promise<void>;
11
12
  back?(page: Page): Promise<void>;
12
13
  repeat?(): number;
@@ -9,5 +9,23 @@
9
9
  "enabled": false,
10
10
  "model": "Claude3.7",
11
11
  "temperature": 0.3
12
+ },
13
+ "envs": {
14
+ "LOCALHOST": "http://localhost:3000",
15
+ "D1": "https://encompass.d1.ice.com",
16
+ "D2": "https://encompass.d2.ice.com",
17
+ "Q1": "https://encompass.q1.ice.com",
18
+ "Q2": "https://encompass.q2.ice.com",
19
+ "Q3": "https://encompass.q3.ice.com",
20
+ "Q4": "https://encompass.q4.ice.com",
21
+ "Q5": "https://encompass.q5.ice.com",
22
+ "Q6": "https://encompass.q6.ice.com",
23
+ "Q7": "https://encompass.q7.ice.com",
24
+ "I1": "https://encompass.i1.ice.com",
25
+ "PL1": "https://encompass.pl1.ice.com",
26
+ "S1": "https://encompass.s1.ice.com",
27
+ "UAT1": "https://encompass.uat1.ice.com",
28
+ "PR": "https://encompass.pr.ice.com",
29
+ "PROD": "https://encompass.elliemae.io"
12
30
  }
13
31
  }
@@ -22,6 +22,11 @@
22
22
  "model": { "type": "string", "minLength": 1 },
23
23
  "temperature": { "type": "number", "minimum": 0, "maximum": 2 }
24
24
  }
25
+ },
26
+ "envs": {
27
+ "type": "object",
28
+ "description": "Map of environment name -> base URL. Selected at runtime via --env <name> or ENCW_ENV.",
29
+ "additionalProperties": { "type": "string", "minLength": 1 }
25
30
  }
26
31
  }
27
32
  }
@@ -12,12 +12,16 @@ import {
12
12
  import { EnvVarConfigSource } from '../../config/sources/envVarConfigSource.js';
13
13
  import { FileConfigSource } from '../../config/sources/fileConfigSource.js';
14
14
  import { RunnerConfigLoader } from '../../config/runnerConfigLoader.js';
15
- import { MissingRequiredParamError } from '../../config/requiredEnvParams.js';
15
+ import {
16
+ MissingRequiredParamError,
17
+ UnknownEnvError,
18
+ } from '../../config/requiredEnvParams.js';
16
19
 
17
20
  interface RunOptions extends CliRunnerOverrides {
18
21
  all?: boolean;
19
22
  tag?: string;
20
23
  baseUrl?: string;
24
+ env?: string;
21
25
  instanceId?: string;
22
26
  userId?: string;
23
27
  configFile?: string;
@@ -33,6 +37,10 @@ export class RunCommand implements CliCommand {
33
37
  .option('--all', 'Run all registered scenarios')
34
38
  .option('--tag <tag>', 'Run all scenarios matching a tag')
35
39
  .option('--base-url <url>', 'Override base URL')
40
+ .option(
41
+ '--env <name>',
42
+ 'Select a named environment from leak-runner.config.json envs (also reads ENCW_ENV)',
43
+ )
36
44
  .option('--instance-id <id>', 'Override instance ID')
37
45
  .option('--user-id <id>', 'Override user ID')
38
46
  .option('--output-dir <dir>', 'Override output directory')
@@ -54,7 +62,10 @@ export class RunCommand implements CliCommand {
54
62
  await new JunitReporter().write(summary, config.runner.outputDir);
55
63
  process.exit(summary.failCount > 0 ? 1 : 0);
56
64
  } catch (err) {
57
- if (err instanceof MissingRequiredParamError) {
65
+ if (
66
+ err instanceof MissingRequiredParamError ||
67
+ err instanceof UnknownEnvError
68
+ ) {
58
69
  process.stderr.write(`Configuration error: ${err.message}\n`);
59
70
  process.exit(2);
60
71
  }
@@ -84,6 +95,8 @@ export class RunCommand implements CliCommand {
84
95
 
85
96
  const env = deps.envParams.resolve({
86
97
  baseUrl: options.baseUrl,
98
+ env: options.env,
99
+ envs: resolved.envs,
87
100
  instanceId: options.instanceId,
88
101
  userId: options.userId,
89
102
  });
@@ -1,13 +1,16 @@
1
1
  import type { EnvironmentParams } from '../types/config.js';
2
2
  import { MissingRequiredParamError } from './missingRequiredParamError.js';
3
+ import { UnknownEnvError } from './unknownEnvError.js';
3
4
 
4
5
  export interface CliEnvOpts {
5
6
  readonly baseUrl?: string;
7
+ readonly env?: string;
8
+ readonly envs?: Readonly<Record<string, string>>;
6
9
  readonly instanceId?: string;
7
10
  readonly userId?: string;
8
11
  }
9
12
 
10
- export { MissingRequiredParamError };
13
+ export { MissingRequiredParamError, UnknownEnvError };
11
14
 
12
15
  export class RequiredEnvParamsResolver {
13
16
  resolve(cliOpts: CliEnvOpts): EnvironmentParams {
@@ -23,8 +26,18 @@ export class RequiredEnvParamsResolver {
23
26
  return params;
24
27
  }
25
28
 
29
+ // Precedence: --base-url > BASE_URL env > --env / ENCW_ENV map lookup.
30
+ // Throws UnknownEnvError when an env name is supplied but not in the map,
31
+ // so a typo fails loudly instead of silently falling through to "missing baseUrl".
26
32
  private resolveBaseUrl(cliOpts: CliEnvOpts): string {
27
- return cliOpts.baseUrl || process.env.BASE_URL || '';
33
+ if (cliOpts.baseUrl) return cliOpts.baseUrl;
34
+ if (process.env.BASE_URL) return process.env.BASE_URL;
35
+ const envName = cliOpts.env || process.env.ENCW_ENV;
36
+ if (!envName) return '';
37
+ const envs = cliOpts.envs ?? {};
38
+ const fromMap = envs[envName];
39
+ if (!fromMap) throw new UnknownEnvError(envName, Object.keys(envs));
40
+ return fromMap;
28
41
  }
29
42
 
30
43
  private resolveInstanceId(cliOpts: CliEnvOpts): string {
@@ -8,6 +8,7 @@ export interface ResolvedRunnerConfigPartial {
8
8
  readonly aiEnabled: boolean;
9
9
  readonly aiModel: string;
10
10
  readonly aiTemperature: number;
11
+ readonly envs: Readonly<Record<string, string>>;
11
12
  }
12
13
 
13
14
  type MutableRunner = { headless: boolean; outputDir: string; topN: number };
@@ -18,6 +19,12 @@ type AiAccumulator = {
18
19
  aiTemperature: number;
19
20
  };
20
21
 
22
+ type Accumulator = {
23
+ runner: MutableRunner;
24
+ ai: AiAccumulator;
25
+ envs: Record<string, string>;
26
+ };
27
+
21
28
  function applyRunnerPayload(
22
29
  acc: MutableRunner,
23
30
  payload: NonNullable<RawRunnerConfigFile['runner']>,
@@ -40,16 +47,14 @@ function applyAiPayload(
40
47
  };
41
48
  }
42
49
 
43
- function applySource(
44
- acc: { runner: MutableRunner; ai: AiAccumulator },
45
- source: ConfigSource,
46
- ): { runner: MutableRunner; ai: AiAccumulator } {
50
+ function applySource(acc: Accumulator, source: ConfigSource): Accumulator {
47
51
  const payload = source.load();
48
52
  return {
49
53
  runner: payload.runner
50
54
  ? applyRunnerPayload(acc.runner, payload.runner)
51
55
  : acc.runner,
52
56
  ai: payload.ai ? applyAiPayload(acc.ai, payload.ai) : acc.ai,
57
+ envs: payload.envs ? { ...acc.envs, ...payload.envs } : acc.envs,
53
58
  };
54
59
  }
55
60
 
@@ -59,7 +64,7 @@ export class RunnerConfigLoader {
59
64
  resolveOptions(): ResolvedRunnerConfigPartial {
60
65
  const ordered = [...this.sources].sort((a, b) => a.priority - b.priority);
61
66
 
62
- const initial = {
67
+ const initial: Accumulator = {
63
68
  runner: {
64
69
  headless: BUILT_IN_DEFAULTS.runner.headless ?? true,
65
70
  outputDir: BUILT_IN_DEFAULTS.runner.outputDir ?? './leak-reports/',
@@ -70,6 +75,7 @@ export class RunnerConfigLoader {
70
75
  aiModel: BUILT_IN_DEFAULTS.ai.model ?? 'Claude3.7',
71
76
  aiTemperature: BUILT_IN_DEFAULTS.ai.temperature ?? 0.3,
72
77
  },
78
+ envs: {},
73
79
  };
74
80
 
75
81
  const resolved = ordered.reduce(applySource, initial);
@@ -79,6 +85,7 @@ export class RunnerConfigLoader {
79
85
  aiEnabled: resolved.ai.aiEnabled,
80
86
  aiModel: resolved.ai.aiModel,
81
87
  aiTemperature: resolved.ai.aiTemperature,
88
+ envs: resolved.envs,
82
89
  };
83
90
  }
84
91
  }
@@ -16,10 +16,14 @@ export const aiConfigFileSchema = z
16
16
  })
17
17
  .partial();
18
18
 
19
+ export const envsSchema = z.record(z.string().min(1));
20
+
19
21
  export const runnerConfigFileSchema = z
20
22
  .object({
23
+ $schema: z.string().optional(),
21
24
  runner: runnerOptionsSchema,
22
25
  ai: aiConfigFileSchema,
26
+ envs: envsSchema,
23
27
  })
24
28
  .partial()
25
29
  .strict();
@@ -46,4 +46,30 @@ describe('FileConfigSource', () => {
46
46
  const src = new FileConfigSource('/anything');
47
47
  expect(src.priority).toBe(1);
48
48
  });
49
+
50
+ it('parses an envs map of named environment URLs', () => {
51
+ const file = makeTempFile(
52
+ JSON.stringify({
53
+ envs: {
54
+ Q3: 'https://q3.elliemae.io',
55
+ Q4: 'https://q4.elliemae.io',
56
+ LOCALHOST: 'http://localhost:3000',
57
+ },
58
+ }),
59
+ );
60
+ const src = new FileConfigSource(file);
61
+ expect(src.load()).toEqual({
62
+ envs: {
63
+ Q3: 'https://q3.elliemae.io',
64
+ Q4: 'https://q4.elliemae.io',
65
+ LOCALHOST: 'http://localhost:3000',
66
+ },
67
+ });
68
+ });
69
+
70
+ it('rejects an envs map whose URL value is an empty string', () => {
71
+ const file = makeTempFile(JSON.stringify({ envs: { Q3: '' } }));
72
+ const src = new FileConfigSource(file);
73
+ expect(() => src.load()).toThrow(/Invalid config file/);
74
+ });
49
75
  });
@@ -1,5 +1,6 @@
1
1
  import { RequiredEnvParamsResolver } from '../requiredEnvParams.js';
2
2
  import { MissingRequiredParamError } from '../missingRequiredParamError.js';
3
+ import { UnknownEnvError } from '../unknownEnvError.js';
3
4
 
4
5
  describe('RequiredEnvParamsResolver', () => {
5
6
  const ORIGINAL_ENV = process.env;
@@ -7,6 +8,7 @@ describe('RequiredEnvParamsResolver', () => {
7
8
  beforeEach(() => {
8
9
  process.env = { ...ORIGINAL_ENV };
9
10
  delete process.env.BASE_URL;
11
+ delete process.env.ENCW_ENV;
10
12
  delete process.env.ENCW_INSTANCE_ID;
11
13
  delete process.env.ENCW_USER_ID;
12
14
  delete process.env.ENCW_PASSWORD;
@@ -76,8 +78,6 @@ describe('RequiredEnvParamsResolver', () => {
76
78
  process.env.BASE_URL = 'https://q3.elliemae.io';
77
79
  process.env.ENCW_INSTANCE_ID = 'BE1';
78
80
  process.env.ENCW_USER_ID = 'admin';
79
- // ENCW_PASSWORD intentionally unset
80
-
81
81
  const resolver = new RequiredEnvParamsResolver();
82
82
  expect(() => resolver.resolve({})).toThrow(MissingRequiredParamError);
83
83
  });
@@ -110,4 +110,92 @@ describe('RequiredEnvParamsResolver', () => {
110
110
  const resolver = new RequiredEnvParamsResolver();
111
111
  expect(() => resolver.resolve({})).toThrow(/Missing required parameter/);
112
112
  });
113
+
114
+ const ENVS = {
115
+ Q3: 'https://q3.elliemae.io',
116
+ Q4: 'https://q4.elliemae.io',
117
+ } as const;
118
+
119
+ function withSecrets(): void {
120
+ process.env.ENCW_INSTANCE_ID = 'BE1';
121
+ process.env.ENCW_USER_ID = 'admin';
122
+ process.env.ENCW_PASSWORD = 'pw';
123
+ }
124
+
125
+ it('resolves baseUrl from the envs map via --env', () => {
126
+ withSecrets();
127
+ const resolver = new RequiredEnvParamsResolver();
128
+ expect(resolver.resolve({ env: 'Q3', envs: ENVS }).baseUrl).toBe(
129
+ 'https://q3.elliemae.io',
130
+ );
131
+ });
132
+
133
+ it('resolves baseUrl from the envs map via ENCW_ENV', () => {
134
+ withSecrets();
135
+ process.env.ENCW_ENV = 'Q4';
136
+ const resolver = new RequiredEnvParamsResolver();
137
+ expect(resolver.resolve({ envs: ENVS }).baseUrl).toBe(
138
+ 'https://q4.elliemae.io',
139
+ );
140
+ });
141
+
142
+ it('--env beats ENCW_ENV when both are set', () => {
143
+ withSecrets();
144
+ process.env.ENCW_ENV = 'Q4';
145
+ const resolver = new RequiredEnvParamsResolver();
146
+ expect(resolver.resolve({ env: 'Q3', envs: ENVS }).baseUrl).toBe(
147
+ 'https://q3.elliemae.io',
148
+ );
149
+ });
150
+
151
+ it('--base-url beats --env map lookup', () => {
152
+ withSecrets();
153
+ const resolver = new RequiredEnvParamsResolver();
154
+ expect(
155
+ resolver.resolve({
156
+ baseUrl: 'https://explicit.example.com',
157
+ env: 'Q3',
158
+ envs: ENVS,
159
+ }).baseUrl,
160
+ ).toBe('https://explicit.example.com');
161
+ });
162
+
163
+ it('BASE_URL env beats --env map lookup', () => {
164
+ withSecrets();
165
+ process.env.BASE_URL = 'https://from-env-var.example.com';
166
+ const resolver = new RequiredEnvParamsResolver();
167
+ expect(resolver.resolve({ env: 'Q3', envs: ENVS }).baseUrl).toBe(
168
+ 'https://from-env-var.example.com',
169
+ );
170
+ });
171
+
172
+ it('throws UnknownEnvError when --env names an undefined env', () => {
173
+ withSecrets();
174
+ const resolver = new RequiredEnvParamsResolver();
175
+ let caught: unknown;
176
+ try {
177
+ resolver.resolve({ env: 'Q99', envs: ENVS });
178
+ } catch (e) {
179
+ caught = e;
180
+ }
181
+ expect(caught).toBeInstanceOf(UnknownEnvError);
182
+ const err = caught as UnknownEnvError;
183
+ expect(err.message).toContain('Q99');
184
+ expect(err.message).toContain('Q3, Q4');
185
+ });
186
+
187
+ it('throws UnknownEnvError when ENCW_ENV is set but envs map is empty', () => {
188
+ withSecrets();
189
+ process.env.ENCW_ENV = 'Q3';
190
+ const resolver = new RequiredEnvParamsResolver();
191
+ expect(() => resolver.resolve({})).toThrow(UnknownEnvError);
192
+ });
193
+
194
+ it('falls back to MissingRequiredParamError when no baseUrl source is available', () => {
195
+ withSecrets();
196
+ const resolver = new RequiredEnvParamsResolver();
197
+ expect(() => resolver.resolve({ envs: ENVS })).toThrow(
198
+ MissingRequiredParamError,
199
+ );
200
+ });
113
201
  });
@@ -56,4 +56,34 @@ describe('RunnerConfigLoader', () => {
56
56
  const loader = new RunnerConfigLoader([file]);
57
57
  expect(loader.resolveOptions().aiEnabled).toBe(true);
58
58
  });
59
+
60
+ it('defaults envs to an empty map when no source contributes one', () => {
61
+ const loader = new RunnerConfigLoader([]);
62
+ expect(loader.resolveOptions().envs).toEqual({});
63
+ });
64
+
65
+ it('surfaces envs from a file source', () => {
66
+ const file = fakeSource(1, 'file', {
67
+ envs: { Q3: 'https://q3.elliemae.io', Q4: 'https://q4.elliemae.io' },
68
+ });
69
+ const loader = new RunnerConfigLoader([file]);
70
+ expect(loader.resolveOptions().envs).toEqual({
71
+ Q3: 'https://q3.elliemae.io',
72
+ Q4: 'https://q4.elliemae.io',
73
+ });
74
+ });
75
+
76
+ it('higher-priority sources override env URLs key-by-key', () => {
77
+ const file = fakeSource(1, 'file', {
78
+ envs: { Q3: 'https://q3.elliemae.io', Q4: 'https://q4.elliemae.io' },
79
+ });
80
+ const cli = fakeSource(3, 'cli', {
81
+ envs: { Q3: 'https://override.example.com' },
82
+ });
83
+ const loader = new RunnerConfigLoader([file, cli]);
84
+ expect(loader.resolveOptions().envs).toEqual({
85
+ Q3: 'https://override.example.com',
86
+ Q4: 'https://q4.elliemae.io',
87
+ });
88
+ });
59
89
  });
@@ -0,0 +1,13 @@
1
+ export class UnknownEnvError extends Error {
2
+ constructor(
3
+ public readonly name: string,
4
+ public readonly knownEnvs: readonly string[],
5
+ ) {
6
+ const knownList =
7
+ knownEnvs.length > 0 ? knownEnvs.join(', ') : '(none defined)';
8
+ super(
9
+ `Unknown env "${name}". Known envs in leak-runner.config.json: ${knownList}.`,
10
+ );
11
+ this.name = 'UnknownEnvError';
12
+ }
13
+ }
package/lib/index.ts CHANGED
@@ -33,5 +33,6 @@ export { CliOverrideConfigSource } from './config/sources/cliOverrideConfigSourc
33
33
  export {
34
34
  RequiredEnvParamsResolver,
35
35
  MissingRequiredParamError,
36
+ UnknownEnvError,
36
37
  } from './config/requiredEnvParams.js';
37
38
  export { buildProgram, defaultDeps } from './cli/index.js';
@@ -14,9 +14,10 @@ import type { RunnerConfig } from '../types/config.js';
14
14
  *
15
15
  * If the config carries no AI config, return the report's original markdown
16
16
  * untouched (no AI module loaded).
17
- * @param report
18
- * @param config
19
- * @param scenarioName
17
+ * @param {ComparisonReport} report - Heap-doctor comparison report to enhance.
18
+ * @param {RunnerConfig} config - Runner config; only `config.ai` is consulted.
19
+ * @param {string} scenarioName - Scenario name used in stderr warnings.
20
+ * @returns {Promise<string>} Markdown for the report, with or without AI section.
20
21
  */
21
22
  export async function enhanceMarkdownIfConfigured(
22
23
  report: ComparisonReport,
@@ -88,6 +88,8 @@ export class ScenarioRunner {
88
88
  snapshotsDir,
89
89
  );
90
90
 
91
+ if (scenario.setup) await scenario.setup(page, frame);
92
+
91
93
  paths.before = await profiler.captureSnapshot('before');
92
94
  await this.repeatScenarioActions(scenario, page, frame);
93
95
  await forceGarbageCollection(page);
@@ -111,12 +113,17 @@ export class ScenarioRunner {
111
113
  const pageSetup = new PageSetup();
112
114
 
113
115
  await pageSetup.apply(page);
116
+ await page.unroute(PageSetup.BLOCKED_PATTERN);
114
117
  await auth.login(page, {
115
118
  username: this.config.env.userId,
116
119
  password: this.config.env.password,
117
120
  instanceId: this.config.env.instanceId,
118
121
  });
119
- await page.goto(scenario.url());
122
+ process.stderr.write(`[scenarioRunner] post-login URL = ${page.url()}\n`);
123
+ await page.waitForURL(`**${scenario.url()}**`, { timeout: 30_000 });
124
+ process.stderr.write(
125
+ `[scenarioRunner] settled URL before iframe = ${page.url()}\n`,
126
+ );
120
127
 
121
128
  const frame = await resolveIframe(page, scenario.microappSelector);
122
129
  fs.mkdirSync(snapshotsDir, { recursive: true });
@@ -1,5 +1,6 @@
1
1
  import type { MicroappLeakScenario } from '../../types/scenario.js';
2
2
  import {
3
+ AdminLandingPageModel,
3
4
  SelectSettingsPageModel,
4
5
  ExportPageModel,
5
6
  } from './page-models/index.js';
@@ -11,9 +12,20 @@ export const exportNavigationScenario: MicroappLeakScenario = {
11
12
  tags: ['critical'],
12
13
  microappSelector: 'iframe#pui-iframe-container-emAdminUI',
13
14
 
14
- url: () => '/admin/oneadmin/migrate',
15
+ url: () => '/admin',
16
+
17
+ async setup(page) {
18
+ await AdminLandingPageModel.acceptCookiesIfShown(page);
19
+ },
15
20
 
16
21
  async action(page, frame) {
22
+ const path = new URL(page.url()).pathname.replace(/\/$/, '');
23
+ if (path === '/admin') {
24
+ const adminLanding = new AdminLandingPageModel(frame);
25
+ await adminLanding.oneAdminConsoleOption.waitFor({ state: 'visible' });
26
+ await adminLanding.clickOneAdminConsole();
27
+ }
28
+
17
29
  const settings = new SelectSettingsPageModel(frame);
18
30
  await settings.container.waitFor({ state: 'visible' });
19
31
  await settings.expandTreeItem('eFolder');
@@ -0,0 +1,24 @@
1
+ import type { Frame, Locator, Page } from '@playwright/test';
2
+
3
+ export class AdminLandingPageModel {
4
+ constructor(private readonly frame: Frame) {}
5
+
6
+ get oneAdminConsoleOption(): Locator {
7
+ return this.frame.getByText('ONE ADMIN CONSOLE');
8
+ }
9
+
10
+ async clickOneAdminConsole(): Promise<void> {
11
+ await this.oneAdminConsoleOption.click();
12
+ }
13
+
14
+ static async acceptCookiesIfShown(page: Page): Promise<void> {
15
+ try {
16
+ await page
17
+ .locator('#onetrust-group-container')
18
+ .waitFor({ state: 'visible', timeout: 5_000 });
19
+ await page.locator('button', { hasText: 'Accept All Cookies' }).click();
20
+ } catch {
21
+ // OneTrust banner not shown — proceed
22
+ }
23
+ }
24
+ }
@@ -1,2 +1,3 @@
1
+ export { AdminLandingPageModel } from './AdminLandingPageModel.js';
1
2
  export { SelectSettingsPageModel } from './SelectSettingsPageModel.js';
2
3
  export { ExportPageModel } from './ExportPageModel.js';