@solidxai/core 0.1.13-beta.11 → 0.1.13-beta.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"run-from-metadata.js","sourceRoot":"","sources":["../../../src/testing/runner/run-from-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Reporter } from \"../reporter/reporter.types\";\nimport type { TestingMetadata, TestingDataRecord } from \"../contracts/testing-metadata.types\";\nimport type { ApiAdapterOptions } from \"../adapters/api/api.types\";\nimport type { PlaywrightAdapterOptions } from \"../adapters/ui/ui.types\";\nimport { ApiAdapter } from \"../adapters/api/api-adapter\";\nimport { registerApiSteps } from \"../steps/api\";\nimport { registerUiSteps } from \"../steps/ui\";\nimport { registerAssertSteps } from \"../steps/assert\";\nimport { registerUtilSteps } from \"../steps/util\";\nimport { registerTestSteps } from \"../steps/test\";\nimport { SimpleResourceStore } from \"../core/resource-store\";\nimport { StepRegistry } from \"../core/step-registry\";\nimport { SpecRegistry } from \"../core/spec-registry\";\nimport { TestingEngine } from \"../core/testing-engine\";\nimport { filterScenarios } from \"./scenario-filter\";\nimport { ensureUiStarted, scenarioNeedsUi } from \"./lifecycle\";\nimport { ConsoleReporter } from \"../reporter/console-reporter\";\n\n\nfunction buildTestDataIndex(data?: TestingDataRecord[]): Record<string, Record<string, any>> {\n const index: Record<string, Record<string, any>> = {};\n if (!Array.isArray(data)) return index;\n for (const record of data) {\n if (!record?.modelUserKey || !record?.recUserKeyValue) continue;\n if (!index[record.modelUserKey]) {\n index[record.modelUserKey] = {};\n }\n index[record.modelUserKey][record.recUserKeyValue] = record.data ?? {};\n }\n return index;\n}\n\nexport type RunnerOptions = {\n metadata: TestingMetadata;\n scenarioIds?: string[];\n includeTags?: string[];\n skipScenarioIds?: string[];\n reporter?: Reporter;\n api?: ApiAdapterOptions;\n ui?: PlaywrightAdapterOptions;\n defaults?: { timeoutMs?: number; retries?: number };\n options?: { printApiLogs?: boolean };\n specs?: (registry: SpecRegistry) => void;\n};\n\nexport async function runFromMetadata(opts: RunnerOptions): Promise<void> {\n const startedAt = Date.now();\n const registry = new StepRegistry();\n registerApiSteps(registry);\n registerUiSteps(registry);\n registerAssertSteps(registry);\n registerUtilSteps(registry);\n registerTestSteps(registry);\n\n const engine = new TestingEngine(registry, opts.defaults);\n const scenarios = filterScenarios(opts.metadata.testing.scenarios, {\n scenarioIds: opts.scenarioIds,\n includeTags: opts.includeTags,\n skipScenarioIds: opts.skipScenarioIds,\n });\n\n const specRegistry = new SpecRegistry();\n const testData = buildTestDataIndex(opts.metadata.testing?.data);\n if (opts.specs) {\n opts.specs(specRegistry);\n }\n\n const resources = new SimpleResourceStore();\n const reporter = opts.reporter ?? new ConsoleReporter();\n const api = new ApiAdapter(opts.api);\n const { PlaywrightAdapter } = await import(\"../adapters/ui/playwright-adapter\");\n const ui = new PlaywrightAdapter(opts.ui);\n const ctxBase = { resources, reporter, api, ui, specRegistry, testData, options: opts.options };\n const uiStarted = { value: false };\n let passed = 0;\n let failed = 0;\n let runError: unknown;\n\n try {\n for (const scenario of scenarios) {\n if (scenarioNeedsUi(scenario)) {\n await ensureUiStarted(ctxBase, uiStarted);\n }\n try {\n await engine.runScenario(scenario, ctxBase);\n passed += 1;\n } catch (error) {\n failed += 1;\n runError = error;\n throw error;\n }\n }\n } finally {\n reporter.onRunEnd?.({\n ok: !runError,\n total: scenarios.length,\n passed,\n failed,\n durationMs: Date.now() - startedAt,\n });\n if (uiStarted.value) {\n await ui.stop();\n }\n }\n}\n"]}
1
+ {"version":3,"file":"run-from-metadata.js","sourceRoot":"","sources":["../../../src/testing/runner/run-from-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Reporter } from \"../reporter/reporter.types\";\nimport type { TestingMetadata, TestingDataRecord } from \"../contracts/testing-metadata.types\";\nimport type { ApiAdapterOptions } from \"../adapters/api/api.types\";\nimport type { PlaywrightAdapterOptions } from \"../adapters/ui/ui.types\";\nimport { ApiAdapter } from \"../adapters/api/api-adapter\";\nimport { registerApiSteps } from \"../steps/api\";\nimport { registerUiSteps } from \"../steps/ui\";\nimport { registerAssertSteps } from \"../steps/assert\";\nimport { registerUtilSteps } from \"../steps/util\";\nimport { registerTestSteps } from \"../steps/test\";\nimport { SimpleResourceStore } from \"../core/resource-store\";\nimport { StepRegistry } from \"../core/step-registry\";\nimport { SpecRegistry } from \"../core/spec-registry\";\nimport { TestingEngine } from \"../core/testing-engine\";\nimport { filterScenarios } from \"./scenario-filter\";\nimport { ensureUiStarted, scenarioNeedsUi } from \"./lifecycle\";\nimport { ConsoleReporter } from \"../reporter/console-reporter\";\n\n\nfunction buildTestDataIndex(data?: TestingDataRecord[]): Record<string, Record<string, any>> {\n const index: Record<string, Record<string, any>> = {};\n if (!Array.isArray(data)) return index;\n for (const record of data) {\n if (!record?.modelUserKey || !record?.recUserKeyValue) continue;\n if (!index[record.modelUserKey]) {\n index[record.modelUserKey] = {};\n }\n index[record.modelUserKey][record.recUserKeyValue] = record.data ?? {};\n }\n return index;\n}\n\nexport type RunnerOptions = {\n metadata: TestingMetadata;\n scenarioIds?: string[];\n includeTags?: string[];\n skipScenarioIds?: string[];\n reporter?: Reporter;\n api?: ApiAdapterOptions;\n ui?: PlaywrightAdapterOptions;\n defaults?: { timeoutMs?: number; retries?: number };\n options?: { printApiLogs?: boolean };\n specs?: (registry: SpecRegistry) => void;\n};\n\nexport async function runFromMetadata(opts: RunnerOptions): Promise<void> {\n const startedAt = Date.now();\n const registry = new StepRegistry();\n registerApiSteps(registry);\n registerUiSteps(registry);\n registerAssertSteps(registry);\n registerUtilSteps(registry);\n registerTestSteps(registry);\n\n const engine = new TestingEngine(registry, opts.defaults);\n const scenarios = filterScenarios(opts.metadata.testing.scenarios, {\n scenarioIds: opts.scenarioIds,\n includeTags: opts.includeTags,\n skipScenarioIds: opts.skipScenarioIds,\n });\n\n const specRegistry = new SpecRegistry();\n const testData = buildTestDataIndex(opts.metadata.testing?.data);\n if (opts.specs) {\n opts.specs(specRegistry);\n }\n\n const resources = new SimpleResourceStore();\n const reporter = opts.reporter ?? new ConsoleReporter();\n reporter.onRunStart?.({ total: scenarios.length });\n const api = new ApiAdapter(opts.api);\n const { PlaywrightAdapter } = await import(\"../adapters/ui/playwright-adapter\");\n const ui = new PlaywrightAdapter(opts.ui);\n const ctxBase = { resources, reporter, api, ui, specRegistry, testData, options: opts.options };\n const uiStarted = { value: false };\n let passed = 0;\n let failed = 0;\n let runError: unknown;\n\n try {\n for (const scenario of scenarios) {\n if (scenarioNeedsUi(scenario)) {\n await ensureUiStarted(ctxBase, uiStarted);\n }\n try {\n await engine.runScenario(scenario, ctxBase);\n passed += 1;\n } catch (error) {\n failed += 1;\n runError = error;\n throw error;\n }\n }\n } finally {\n reporter.onRunEnd?.({\n ok: !runError,\n total: scenarios.length,\n passed,\n failed,\n durationMs: Date.now() - startedAt,\n });\n if (uiStarted.value) {\n await ui.stop();\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidxai/core",
3
- "version": "0.1.13-beta.11",
3
+ "version": "0.1.13-beta.12",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -3,6 +3,7 @@ import { SubCommand, CommandRunner, Option } from 'nest-commander';
3
3
  import * as path from 'path';
4
4
  import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
5
5
  import { ConsoleReporter } from 'src/testing/reporter/console-reporter';
6
+ import { ProgressReporter } from 'src/testing/reporter/progress-reporter';
6
7
  import { WebhookReporter } from 'src/testing/reporter/webhook-reporter';
7
8
  import { runFromMetadata } from 'src/testing/runner/run-from-metadata';
8
9
  import type { TestingMetadata } from 'src/testing/contracts/testing-metadata.types';
@@ -27,6 +28,7 @@ interface TestRunCommandOptions {
27
28
  listSpecs?: boolean;
28
29
  printApiLogs?: boolean;
29
30
  runName?: string;
31
+ progress?: boolean;
30
32
  }
31
33
 
32
34
  @SubCommand({
@@ -106,9 +108,12 @@ export class TestRunCommand extends CommandRunner {
106
108
 
107
109
  const webhookUrl = process.env.SOLIDCTL_WEBHOOK_URL;
108
110
  const runName = options?.runName ?? `run-${Date.now()}`;
109
- const reporter = webhookUrl
111
+ const baseReporter = webhookUrl
110
112
  ? new WebhookReporter(webhookUrl, runName)
111
113
  : new ConsoleReporter();
114
+ const reporter = options?.progress === false
115
+ ? baseReporter
116
+ : new ProgressReporter(baseReporter);
112
117
 
113
118
  try {
114
119
  await runFromMetadata({
@@ -138,8 +143,8 @@ export class TestRunCommand extends CommandRunner {
138
143
  console.error('Run tests command failed');
139
144
  process.exitCode = 1;
140
145
  } finally {
141
- if (reporter instanceof WebhookReporter) {
142
- await reporter.flush(Number(process.exitCode ?? 0));
146
+ if (baseReporter instanceof WebhookReporter) {
147
+ await baseReporter.flush(Number(process.exitCode ?? 0));
143
148
  }
144
149
  }
145
150
  return;
@@ -289,6 +294,15 @@ export class TestRunCommand extends CommandRunner {
289
294
  parseRunName(val: string): string {
290
295
  return val;
291
296
  }
297
+
298
+ @Option({
299
+ flags: '--no-progress',
300
+ description: 'Disable the interactive test progress footer.',
301
+ required: false,
302
+ })
303
+ parseNoProgress(): boolean {
304
+ return false;
305
+ }
292
306
  }
293
307
 
294
308
  function splitCsv(value?: string): string[] | undefined {
@@ -479,10 +479,11 @@ Common `test run` flags (all pass through solidctl unchanged):
479
479
  - `--ui-navigation-timeout-ms <number>` — default wait for `ui.goto`
480
480
  - `--timeout-ms <number>` / `--retries <number>` — scenario-level guard and retry count
481
481
  - `--scenario-ids <ids>` / `--include-tags <tags>` / `--skip-scenario-ids <ids>`
482
+ - `--no-progress` — disable the interactive test progress footer
482
483
 
483
484
  ## Add A New Step (SOP)
484
485
  1. Create a new `*.step.ts` in the right domain folder.
485
486
  2. Implement a `registerXSteps(registry)` function and `registry.register("op.name", handler)`.
486
487
  3. Validate required `step.with` fields and throw clear errors.
487
488
  4. Use adapters via `ctx.api` / `ctx.ui` and update `ctx.last` when helpful.
488
- 5. Export and register it from the domain `index.ts`.
489
+ 5. Export and register it from the domain `index.ts`.
@@ -150,6 +150,10 @@ export class ConsoleReporter implements Reporter {
150
150
  private passedScenarios = 0;
151
151
  private failedScenarios = 0;
152
152
 
153
+ onRunStart(_args: { total: number }): void {
154
+ // Progress-aware reporters use this hook; the plain console reporter has no setup.
155
+ }
156
+
153
157
  onScenarioStart(scenario: { id: string; name?: string }): void {
154
158
  const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
155
159
  console.log(`\n▶ Scenario: ${label}`);
@@ -0,0 +1,251 @@
1
+ import * as readline from "readline";
2
+ import type { StepPhase } from "../contracts/runtime-context.types";
3
+ import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types";
4
+ import type { SolidTestSpecResult } from "../contracts/test-spec.types";
5
+ import type { Reporter } from "./reporter.types";
6
+
7
+ const ANSI_BOLD = "\u001b[1m";
8
+ const ANSI_BOLD_OFF = "\u001b[22m";
9
+ const ANSI_DIM = "\u001b[2m";
10
+ const ANSI_GREEN = "\u001b[32m";
11
+ const ANSI_COLOR_OFF = "\u001b[39m";
12
+ const ANSI_RED = "\u001b[31m";
13
+ const ANSI_INVERSE = "\u001b[7m";
14
+ const ANSI_INVERSE_OFF = "\u001b[27m";
15
+
16
+ export class ProgressReporter implements Reporter {
17
+ private startedAt = Date.now();
18
+ private total = 0;
19
+ private completed = 0;
20
+ private passed = 0;
21
+ private failed = 0;
22
+ private timer?: NodeJS.Timeout;
23
+ private rendered = false;
24
+
25
+ constructor(private readonly delegate: Reporter) {}
26
+
27
+ onRunStart(args: { total: number }): void {
28
+ this.total = args.total;
29
+ this.startedAt = Date.now();
30
+ this.completed = 0;
31
+ this.passed = 0;
32
+ this.failed = 0;
33
+ this.delegate.onRunStart?.(args);
34
+ this.startTimer();
35
+ this.renderFooter();
36
+ }
37
+
38
+ onScenarioStart(scenario: ScenarioSpec): void {
39
+ this.withFooterPaused(() => this.delegate.onScenarioStart(scenario));
40
+ }
41
+
42
+ onScenarioEnd(
43
+ scenario: ScenarioSpec,
44
+ result: { ok: boolean; error?: unknown; durationMs: number },
45
+ ): void {
46
+ this.completed += 1;
47
+ if (result.ok) {
48
+ this.passed += 1;
49
+ } else {
50
+ this.failed += 1;
51
+ }
52
+ this.withFooterPaused(() => this.delegate.onScenarioEnd(scenario, result));
53
+ }
54
+
55
+ onStepStart(args: {
56
+ scenarioId: string;
57
+ phase: StepPhase;
58
+ step: OpStep;
59
+ }): void {
60
+ this.withFooterPaused(() => this.delegate.onStepStart(args));
61
+ }
62
+
63
+ onStepEnd(args: {
64
+ scenarioId: string;
65
+ phase: StepPhase;
66
+ step: OpStep;
67
+ ok: boolean;
68
+ error?: unknown;
69
+ durationMs: number;
70
+ }): void {
71
+ this.withFooterPaused(() => this.delegate.onStepEnd(args));
72
+ }
73
+
74
+ onSpecResult(args: {
75
+ scenarioId: string;
76
+ specId: string;
77
+ stepName?: string;
78
+ result: SolidTestSpecResult;
79
+ }): void {
80
+ this.withFooterPaused(() => this.delegate.onSpecResult?.(args));
81
+ }
82
+
83
+ attach(args: {
84
+ scenarioId: string;
85
+ name: string;
86
+ contentType: string;
87
+ data: Buffer | string;
88
+ }): void {
89
+ this.withFooterPaused(() => this.delegate.attach?.(args));
90
+ }
91
+
92
+ onRunEnd(args: {
93
+ ok: boolean;
94
+ total: number;
95
+ passed: number;
96
+ failed: number;
97
+ durationMs: number;
98
+ }): void {
99
+ this.completed = args.passed + args.failed;
100
+ this.passed = args.passed;
101
+ this.failed = args.failed;
102
+ this.total = args.total;
103
+ this.stopTimer();
104
+ this.withFooterPaused(() => this.delegate.onRunEnd?.(args), false);
105
+ }
106
+
107
+ private startTimer(): void {
108
+ this.stopTimer();
109
+ if (!this.isInteractive()) {
110
+ return;
111
+ }
112
+ this.timer = setInterval(() => this.renderFooter(), 1_000);
113
+ this.timer.unref?.();
114
+ }
115
+
116
+ private stopTimer(): void {
117
+ if (!this.timer) {
118
+ return;
119
+ }
120
+ clearInterval(this.timer);
121
+ this.timer = undefined;
122
+ }
123
+
124
+ private withFooterPaused(callback: () => void, renderAfter = true): void {
125
+ this.clearFooter();
126
+ callback();
127
+ if (renderAfter) {
128
+ this.renderFooter();
129
+ }
130
+ }
131
+
132
+ private renderFooter(): void {
133
+ if (!this.isInteractive()) {
134
+ return;
135
+ }
136
+
137
+ const terminalWidth = process.stdout.columns || 80;
138
+ const elapsed = formatElapsed(Date.now() - this.startedAt);
139
+ const progressText = `${this.completed}/${this.total}`;
140
+ const resultText = `${ANSI_GREEN}${this.passed} passed${ANSI_COLOR_OFF}${ANSI_DIM} | ${ANSI_BOLD_OFF}${this.failed ? ANSI_RED : ANSI_DIM}${this.failed} failed${this.failed ? ANSI_COLOR_OFF : ANSI_BOLD_OFF}`;
141
+ const footer = [
142
+ `${ANSI_BOLD}Tests${ANSI_BOLD_OFF}: ${this.renderProgressBar(terminalWidth)} ${progressText}`,
143
+ `${ANSI_BOLD}Elapsed${ANSI_BOLD_OFF}: ${elapsed}`,
144
+ resultText,
145
+ ].join(`${ANSI_DIM} | ${ANSI_BOLD_OFF}`);
146
+
147
+ readline.clearLine(process.stdout, 0);
148
+ readline.cursorTo(process.stdout, 0);
149
+ process.stdout.write(truncateAnsiText(`${ANSI_INVERSE} ${footer} ${ANSI_INVERSE_OFF}`, terminalWidth));
150
+ this.rendered = true;
151
+ }
152
+
153
+ private clearFooter(): void {
154
+ if (!this.rendered || !this.isInteractive()) {
155
+ return;
156
+ }
157
+ readline.clearLine(process.stdout, 0);
158
+ readline.cursorTo(process.stdout, 0);
159
+ this.rendered = false;
160
+ }
161
+
162
+ private renderProgressBar(terminalWidth: number): string {
163
+ const width = terminalWidth >= 120 ? 30 : terminalWidth >= 90 ? 22 : 14;
164
+ const ratio = this.total > 0 ? Math.min(1, this.completed / this.total) : 0;
165
+ const filled = Math.round(width * ratio);
166
+ const empty = Math.max(0, width - filled);
167
+ return `[${ANSI_GREEN}${"=".repeat(filled)}${ANSI_COLOR_OFF}${" ".repeat(empty)}]`;
168
+ }
169
+
170
+ private isInteractive(): boolean {
171
+ return Boolean(process.stdout.isTTY || process.env.SOLIDCTL_TEST_PROGRESS === "1");
172
+ }
173
+ }
174
+
175
+ function formatElapsed(durationMs: number): string {
176
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
177
+ const minutes = Math.floor(totalSeconds / 60);
178
+ const seconds = totalSeconds % 60;
179
+ if (minutes < 60) {
180
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
181
+ }
182
+ const hours = Math.floor(minutes / 60);
183
+ const remainingMinutes = minutes % 60;
184
+ return `${hours}:${remainingMinutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
185
+ }
186
+
187
+ function truncateAnsiText(text: string, maxWidth: number): string {
188
+ if (maxWidth <= 0) {
189
+ return "";
190
+ }
191
+
192
+ let visibleWidth = 0;
193
+ let index = 0;
194
+ let output = "";
195
+ let hasEscape = false;
196
+
197
+ while (index < text.length) {
198
+ if (text[index] === "\u001b") {
199
+ const match = /\u001b\[[0-9;]*m/.exec(text.slice(index));
200
+ if (!match) {
201
+ break;
202
+ }
203
+ output += match[0];
204
+ hasEscape = true;
205
+ index += match[0].length;
206
+ continue;
207
+ }
208
+
209
+ if (visibleWidth >= maxWidth) {
210
+ break;
211
+ }
212
+
213
+ output += text[index];
214
+ visibleWidth += 1;
215
+ index += 1;
216
+ }
217
+
218
+ if (index < text.length && maxWidth >= 1) {
219
+ output = trimAnsiText(output, maxWidth - 1) + "…";
220
+ }
221
+
222
+ if (hasEscape && !output.endsWith(ANSI_INVERSE_OFF)) {
223
+ output += ANSI_INVERSE_OFF;
224
+ }
225
+
226
+ return output;
227
+ }
228
+
229
+ function trimAnsiText(text: string, maxWidth: number): string {
230
+ let visibleWidth = 0;
231
+ let index = 0;
232
+ let output = "";
233
+
234
+ while (index < text.length && visibleWidth < maxWidth) {
235
+ if (text[index] === "\u001b") {
236
+ const match = /\u001b\[[0-9;]*m/.exec(text.slice(index));
237
+ if (!match) {
238
+ break;
239
+ }
240
+ output += match[0];
241
+ index += match[0].length;
242
+ continue;
243
+ }
244
+
245
+ output += text[index];
246
+ visibleWidth += 1;
247
+ index += 1;
248
+ }
249
+
250
+ return output;
251
+ }
@@ -3,6 +3,9 @@ import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types";
3
3
  import type { SolidTestSpecResult } from "../contracts/test-spec.types";
4
4
 
5
5
  export interface Reporter {
6
+ onRunStart?(args: {
7
+ total: number;
8
+ }): void;
6
9
  onScenarioStart(scenario: ScenarioSpec): void;
7
10
  onScenarioEnd(
8
11
  scenario: ScenarioSpec,
@@ -67,6 +67,7 @@ export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
67
67
 
68
68
  const resources = new SimpleResourceStore();
69
69
  const reporter = opts.reporter ?? new ConsoleReporter();
70
+ reporter.onRunStart?.({ total: scenarios.length });
70
71
  const api = new ApiAdapter(opts.api);
71
72
  const { PlaywrightAdapter } = await import("../adapters/ui/playwright-adapter");
72
73
  const ui = new PlaywrightAdapter(opts.ui);