qase-javascript-commons 2.6.2 → 2.6.3

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/changelog.md CHANGED
@@ -1,3 +1,50 @@
1
+ # qase-javascript-commons@2.6.3
2
+
3
+ ## Internal refactoring
4
+
5
+ The `QaseReporter` God class (~680 lines) has been split into focused components,
6
+ and spec-compliant report serialization has been extracted into its own module.
7
+ **The public API is unchanged** — all exports, method signatures, option shapes,
8
+ environment variables, and the JSON report format are preserved verbatim. The
9
+ report format is additionally verified in CI against the official Qase report
10
+ schemas via `reporters-validator`.
11
+
12
+ New internal components:
13
+
14
+ - `src/qase/options-resolver.ts` — env + config composition, state restore,
15
+ `withState` detection.
16
+ - `src/qase/reporter-factory.ts` — mode-based reporter instantiation with
17
+ option validation (replaces the inline `switch` that previously lived inside
18
+ `QaseReporter.createReporter`).
19
+ - `src/qase/status-processor.ts` — status mapping + filtering.
20
+ - `src/reporters/shared/fallback-coordinator.ts` — encapsulates the
21
+ upstream → fallback → disabled cascade that was previously repeated in five
22
+ lifecycle methods of `QaseReporter`.
23
+ - `src/utils/token-masker.ts` — reusable `maskToken` / `sanitizeOptionsForLog`.
24
+ - `src/formatter/report-serializer.ts` — pure spec-compliant serializer
25
+ (RSLT-01/02, STEP-01/02/03 rules), lifted out of `ReportReporter`.
26
+
27
+ Shared utilities:
28
+
29
+ - `DEFAULT_BATCH_SIZE` constant and `resolveTestOpsBaseUrl` helper now live
30
+ in `src/reporters/shared/` (previously duplicated between `TestOpsReporter`
31
+ and `TestOpsMultiReporter`).
32
+
33
+ ## Bug fixes
34
+
35
+ - **Fallback activation on upstream creation failure.** Previously, if the
36
+ upstream reporter failed to construct (for example, misconfigured TestOps
37
+ client) and a `fallback` mode was configured, the reporter would disable
38
+ itself instead of switching to the fallback. Now the fallback is correctly
39
+ activated. **Note for users:** pipelines with a broken upstream
40
+ configuration and a `report` fallback will now start producing local report
41
+ artifacts where previously nothing was produced.
42
+ - `publish()` no longer double-calls both reporters when already in fallback
43
+ mode — it now runs exactly one reporter.
44
+ - `StateManager.setMode(off)` on fallback failure now respects the
45
+ `withState` flag, so frameworks that don't use persistent state no longer
46
+ write state during shutdown.
47
+
1
48
  # qase-javascript-commons@2.6.2
2
49
 
3
50
  ## Bug fixes
@@ -1,3 +1,4 @@
1
1
  export { type FormatterInterface } from './formatter-interface';
2
2
  export { JsonFormatter } from './json-formatter';
3
3
  export { JsonpFormatter } from './jsonp-formatter';
4
+ export { ReportSerializer, type ReportSerializerInterface } from './report-serializer';
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JsonpFormatter = exports.JsonFormatter = void 0;
3
+ exports.ReportSerializer = exports.JsonpFormatter = exports.JsonFormatter = void 0;
4
4
  var json_formatter_1 = require("./json-formatter");
5
5
  Object.defineProperty(exports, "JsonFormatter", { enumerable: true, get: function () { return json_formatter_1.JsonFormatter; } });
6
6
  var jsonp_formatter_1 = require("./jsonp-formatter");
7
7
  Object.defineProperty(exports, "JsonpFormatter", { enumerable: true, get: function () { return jsonp_formatter_1.JsonpFormatter; } });
8
+ var report_serializer_1 = require("./report-serializer");
9
+ Object.defineProperty(exports, "ReportSerializer", { enumerable: true, get: function () { return report_serializer_1.ReportSerializer; } });
@@ -0,0 +1,20 @@
1
+ import { Attachment, TestResultType, TestStepType } from '../models';
2
+ export interface ReportSerializerInterface {
3
+ serializeResult(result: TestResultType): Record<string, unknown>;
4
+ serializeStep(step: TestStepType): Record<string, unknown>;
5
+ serializeAttachment(att: Attachment): Record<string, unknown>;
6
+ }
7
+ /**
8
+ * Transforms internal TestResultType / TestStepType into Qase Report
9
+ * spec-compliant JSON (RSLT-01/02, STEP-01/02/03 rules).
10
+ *
11
+ * Pure: no I/O, no logger, no state. Suitable for golden-testing.
12
+ */
13
+ export declare class ReportSerializer implements ReportSerializerInterface {
14
+ serializeResult(result: TestResultType): Record<string, unknown>;
15
+ serializeStep(step: TestStepType): Record<string, unknown>;
16
+ serializeAttachment(att: Attachment): Record<string, unknown>;
17
+ private transformTestopsIds;
18
+ private transformGroupParams;
19
+ private serializeStepData;
20
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReportSerializer = void 0;
4
+ const models_1 = require("../models");
5
+ /**
6
+ * Transforms internal TestResultType / TestStepType into Qase Report
7
+ * spec-compliant JSON (RSLT-01/02, STEP-01/02/03 rules).
8
+ *
9
+ * Pure: no I/O, no logger, no state. Suitable for golden-testing.
10
+ */
11
+ class ReportSerializer {
12
+ serializeResult(result) {
13
+ const testopsIds = this.transformTestopsIds(result.testops_id);
14
+ const paramGroups = this.transformGroupParams(result.group_params);
15
+ return {
16
+ id: result.id,
17
+ title: result.title,
18
+ signature: result.signature,
19
+ execution: result.execution,
20
+ fields: result.fields,
21
+ attachments: result.attachments.map((a) => this.serializeAttachment(a)),
22
+ steps: result.steps.map((s) => this.serializeStep(s)),
23
+ params: result.params,
24
+ param_groups: paramGroups,
25
+ testops_ids: testopsIds,
26
+ relations: result.relations,
27
+ muted: result.muted,
28
+ message: result.message,
29
+ tags: result.tags,
30
+ };
31
+ }
32
+ serializeStep(step) {
33
+ const data = this.serializeStepData(step);
34
+ const attachments = step.attachments.map((a) => this.serializeAttachment(a));
35
+ return {
36
+ id: step.id,
37
+ step_type: step.step_type,
38
+ data,
39
+ parent_id: step.parent_id,
40
+ execution: { ...step.execution, attachments },
41
+ steps: step.steps.map((s) => this.serializeStep(s)),
42
+ };
43
+ }
44
+ serializeAttachment(att) {
45
+ return {
46
+ id: att.id,
47
+ file_name: att.file_name,
48
+ mime_type: att.mime_type,
49
+ file_path: att.file_path,
50
+ };
51
+ }
52
+ transformTestopsIds(testopsId) {
53
+ if (testopsId === null)
54
+ return null;
55
+ return Array.isArray(testopsId)
56
+ ? testopsId
57
+ : [testopsId];
58
+ }
59
+ transformGroupParams(groupParams) {
60
+ const keys = Object.keys(groupParams);
61
+ return keys.length === 0 ? [] : [keys];
62
+ }
63
+ serializeStepData(step) {
64
+ const data = { ...step.data };
65
+ if (step.step_type === models_1.StepType.TEXT && 'data' in data) {
66
+ const textData = data;
67
+ return {
68
+ action: textData.action,
69
+ expected_result: textData.expected_result,
70
+ input_data: textData.data,
71
+ };
72
+ }
73
+ if (step.step_type === models_1.StepType.GHERKIN &&
74
+ 'keyword' in data &&
75
+ 'name' in data) {
76
+ const gherkinData = data;
77
+ return {
78
+ action: `${gherkinData.keyword} ${gherkinData.name}`,
79
+ expected_result: null,
80
+ input_data: null,
81
+ };
82
+ }
83
+ if (step.step_type === models_1.StepType.REQUEST && 'request_method' in data) {
84
+ return data;
85
+ }
86
+ return data;
87
+ }
88
+ }
89
+ exports.ReportSerializer = ReportSerializer;
@@ -0,0 +1,19 @@
1
+ import { ModeEnum, OptionsType } from '../options';
2
+ import { ConfigType } from '../config';
3
+ export interface ResolvedOptions {
4
+ effectiveMode: ModeEnum;
5
+ effectiveFallback: ModeEnum;
6
+ composed: ConfigType & OptionsType;
7
+ withState: boolean;
8
+ }
9
+ /**
10
+ * Composes the final options used by QaseReporter:
11
+ * - restores mode / runId from state (when withState is active)
12
+ * - merges env-derived config with user options
13
+ * - determines effective mode / fallback values
14
+ */
15
+ export declare class OptionsResolver {
16
+ resolve(options: OptionsType): ResolvedOptions;
17
+ private detectWithState;
18
+ private restoreFromState;
19
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OptionsResolver = void 0;
7
+ const env_schema_1 = __importDefault(require("env-schema"));
8
+ const env_1 = require("../env");
9
+ const options_1 = require("../options");
10
+ const state_1 = require("../state/state");
11
+ /**
12
+ * Composes the final options used by QaseReporter:
13
+ * - restores mode / runId from state (when withState is active)
14
+ * - merges env-derived config with user options
15
+ * - determines effective mode / fallback values
16
+ */
17
+ class OptionsResolver {
18
+ resolve(options) {
19
+ const withState = this.detectWithState(options);
20
+ if (withState) {
21
+ this.restoreFromState();
22
+ }
23
+ const env = (0, env_1.envToConfig)((0, env_schema_1.default)({ schema: env_1.envValidationSchema }));
24
+ const composed = (0, options_1.composeOptions)(options, env);
25
+ return {
26
+ effectiveMode: composed.mode ?? options_1.ModeEnum.off,
27
+ effectiveFallback: composed.fallback ?? options_1.ModeEnum.off,
28
+ composed,
29
+ withState,
30
+ };
31
+ }
32
+ detectWithState(options) {
33
+ return options.frameworkName === 'cypress' || !options.frameworkName;
34
+ }
35
+ restoreFromState() {
36
+ if (!state_1.StateManager.isStateExists())
37
+ return;
38
+ const state = state_1.StateManager.getState();
39
+ if (state.IsModeChanged && state.Mode) {
40
+ process.env[env_1.EnvEnum.mode] = state.Mode.toString();
41
+ }
42
+ if (state.RunId) {
43
+ process.env[env_1.EnvRunEnum.id] = state.RunId.toString();
44
+ }
45
+ }
46
+ }
47
+ exports.OptionsResolver = OptionsResolver;
@@ -0,0 +1,19 @@
1
+ import { InternalReporterInterface } from '../reporters';
2
+ import { ModeEnum, OptionsType } from '../options';
3
+ import { ConfigType } from '../config';
4
+ import { LoggerInterface } from '../utils/logger';
5
+ import { HostData } from '../models/host-data';
6
+ /**
7
+ * Builds a mode-specific InternalReporterInterface. Throws DisabledException
8
+ * for `ModeEnum.off` so callers can distinguish "disabled-by-config" from a
9
+ * real failure.
10
+ */
11
+ export declare class ReporterFactory {
12
+ private readonly logger;
13
+ private readonly hostData;
14
+ constructor(logger: LoggerInterface, hostData: HostData);
15
+ create(mode: ModeEnum, options: ConfigType & OptionsType, withState: boolean): InternalReporterInterface;
16
+ private createTestOps;
17
+ private createTestOpsMulti;
18
+ private createReport;
19
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReporterFactory = void 0;
4
+ const reporters_1 = require("../reporters");
5
+ const options_1 = require("../options");
6
+ const env_1 = require("../env");
7
+ const disabled_exception_1 = require("../utils/disabled-exception");
8
+ const clientV2_1 = require("../client/clientV2");
9
+ const writer_1 = require("../writer");
10
+ /**
11
+ * Builds a mode-specific InternalReporterInterface. Throws DisabledException
12
+ * for `ModeEnum.off` so callers can distinguish "disabled-by-config" from a
13
+ * real failure.
14
+ */
15
+ class ReporterFactory {
16
+ logger;
17
+ hostData;
18
+ constructor(logger, hostData) {
19
+ this.logger = logger;
20
+ this.hostData = hostData;
21
+ }
22
+ create(mode, options, withState) {
23
+ switch (mode) {
24
+ case options_1.ModeEnum.testops:
25
+ return this.createTestOps(options, withState);
26
+ case options_1.ModeEnum.testops_multi:
27
+ return this.createTestOpsMulti(options, withState);
28
+ case options_1.ModeEnum.report:
29
+ return this.createReport(options);
30
+ case options_1.ModeEnum.off:
31
+ throw new disabled_exception_1.DisabledException();
32
+ default:
33
+ throw new Error(`Unknown mode type`);
34
+ }
35
+ }
36
+ createTestOps(options, withState) {
37
+ if (!options.testops?.api?.token) {
38
+ throw new Error(`Either "testops.api.token" parameter or "${env_1.EnvApiEnum.token}" environment variable is required in "testops" mode`);
39
+ }
40
+ if (!options.testops.project) {
41
+ throw new Error(`Either "testops.project" parameter or "${env_1.EnvTestOpsEnum.project}" environment variable is required in "testops" mode`);
42
+ }
43
+ const apiClient = new clientV2_1.ClientV2(this.logger, options.testops, options.environment, options.rootSuite, this.hostData, options.reporterName, options.frameworkPackage);
44
+ return new reporters_1.TestOpsReporter(this.logger, apiClient, withState, options.testops.project, options.testops.api.host, options.testops.batch?.size, options.testops.run?.id, options.testops.showPublicReportLink);
45
+ }
46
+ createTestOpsMulti(options, withState) {
47
+ if (!options.testops?.api?.token) {
48
+ throw new Error(`Either "testops.api.token" parameter or "${env_1.EnvApiEnum.token}" environment variable is required in "testops_multi" mode`);
49
+ }
50
+ const multi = options.testops_multi;
51
+ if (!multi?.projects?.length) {
52
+ throw new Error('"testops_multi.projects" must contain at least one project with a "code" field');
53
+ }
54
+ for (const p of multi.projects) {
55
+ if (!p?.code) {
56
+ throw new Error('Each project in "testops_multi.projects" must have a "code" field');
57
+ }
58
+ }
59
+ return new reporters_1.TestOpsMultiReporter(this.logger, options.testops, multi, withState, this.hostData, options.reporterName, options.frameworkPackage, options.environment, options.testops.api?.host, options.testops.batch?.size, options.testops.showPublicReportLink);
60
+ }
61
+ createReport(options) {
62
+ const localOptions = options.report?.connections?.[writer_1.DriverEnum.local];
63
+ const writer = new writer_1.FsWriter(localOptions);
64
+ return new reporters_1.ReportReporter(this.logger, writer, options.frameworkPackage, options.reporterName, options.environment, options.rootSuite, options.testops?.run?.id, this.hostData);
65
+ }
66
+ }
67
+ exports.ReporterFactory = ReporterFactory;
@@ -0,0 +1,17 @@
1
+ import { TestResultType } from '../models';
2
+ import { LoggerInterface } from '../utils/logger';
3
+ /**
4
+ * Applies status mapping (rename rule) and status filter (drop rule) to a test
5
+ * result. Mutates `result.execution.status` when a mapping rule matches, then
6
+ * returns either the (possibly mutated) result or `null` if it should be
7
+ * filtered out.
8
+ */
9
+ export declare class StatusProcessor {
10
+ private readonly logger;
11
+ private readonly statusMapping;
12
+ private readonly statusFilter;
13
+ constructor(logger: LoggerInterface, statusMapping: Record<string, string> | undefined, statusFilter: string[] | undefined);
14
+ process(result: TestResultType): TestResultType | null;
15
+ private applyMapping;
16
+ private shouldFilter;
17
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatusProcessor = void 0;
4
+ const status_mapping_utils_1 = require("../utils/status-mapping-utils");
5
+ /**
6
+ * Applies status mapping (rename rule) and status filter (drop rule) to a test
7
+ * result. Mutates `result.execution.status` when a mapping rule matches, then
8
+ * returns either the (possibly mutated) result or `null` if it should be
9
+ * filtered out.
10
+ */
11
+ class StatusProcessor {
12
+ logger;
13
+ statusMapping;
14
+ statusFilter;
15
+ constructor(logger, statusMapping, statusFilter) {
16
+ this.logger = logger;
17
+ this.statusMapping = statusMapping;
18
+ this.statusFilter = statusFilter;
19
+ }
20
+ process(result) {
21
+ this.applyMapping(result);
22
+ if (this.shouldFilter(result)) {
23
+ this.logger.logDebug(`Filtering out test result with status: ${result.execution.status}`);
24
+ return null;
25
+ }
26
+ return result;
27
+ }
28
+ applyMapping(result) {
29
+ if (!this.statusMapping)
30
+ return;
31
+ const originalStatus = result.execution.status;
32
+ const mapped = (0, status_mapping_utils_1.applyStatusMapping)(originalStatus, this.statusMapping);
33
+ if (mapped !== originalStatus) {
34
+ this.logger.logDebug(`Status mapping applied: ${originalStatus} -> ${mapped}`);
35
+ result.execution.status = mapped;
36
+ }
37
+ }
38
+ shouldFilter(result) {
39
+ if (!this.statusFilter || this.statusFilter.length === 0)
40
+ return false;
41
+ const statusString = result.execution.status.toString();
42
+ this.logger.logDebug(`Checking filter: status="${statusString}", filter=${JSON.stringify(this.statusFilter)}`);
43
+ const shouldFilter = this.statusFilter.includes(statusString);
44
+ this.logger.logDebug(`Filter result: ${shouldFilter ? 'FILTERED' : 'NOT FILTERED'}`);
45
+ return shouldFilter;
46
+ }
47
+ }
48
+ exports.StatusProcessor = StatusProcessor;
package/dist/qase.d.ts CHANGED
@@ -13,104 +13,36 @@ export interface ReporterInterface {
13
13
  }
14
14
  /**
15
15
  * @class QaseReporter
16
- * @implements AbstractReporter
16
+ *
17
+ * Thin orchestrator: delegates to OptionsResolver, ReporterFactory,
18
+ * StatusProcessor, and FallbackCoordinator. Public API is preserved.
17
19
  */
18
20
  export declare class QaseReporter implements ReporterInterface {
19
21
  private static instance;
20
- /**
21
- * @type {InternalReporterInterface}
22
- * @private
23
- */
24
- private readonly upstreamReporter?;
25
- /**
26
- * @type {InternalReporterInterface}
27
- * @private
28
- */
29
- private readonly fallbackReporter?;
30
- /**
31
- * @type {boolean | undefined}
32
- * @private
33
- */
34
- private readonly captureLogs;
35
- /**
36
- * @type {boolean}
37
- * @private
38
- */
39
- private disabled;
40
- /**
41
- * @type {boolean}
42
- * @private
43
- */
44
- private useFallback;
45
22
  private readonly logger;
23
+ private readonly options;
24
+ private readonly withState;
25
+ private readonly captureLogs;
26
+ private readonly statusProcessor;
27
+ private readonly fallback;
46
28
  private startTestRunOperation?;
47
- private options;
48
- private withState;
49
- private readonly hostData;
50
29
  /**
51
30
  * @param {OptionsType} options
52
31
  */
53
32
  private constructor();
33
+ private buildLogger;
34
+ private buildReporters;
35
+ private persistInitialState;
36
+ static getInstance(options: OptionsType): QaseReporter;
37
+ getStatusMapping(): Record<string, string> | undefined;
54
38
  uploadAttachment(attachment: Attachment): Promise<string>;
55
39
  getResults(): TestResultType[];
56
40
  setTestResults(results: TestResultType[]): void;
57
- sendResults(): Promise<void>;
58
- complete(): Promise<void>;
59
- /**
60
- * @returns {void}
61
- */
41
+ addTestResult(result: TestResultType): Promise<void>;
62
42
  startTestRun(): void;
63
43
  startTestRunAsync(): Promise<void>;
64
- /**
65
- * @param {OptionsType} options
66
- * @returns {QaseReporter}
67
- */
68
- static getInstance(options: OptionsType): QaseReporter;
69
- /**
70
- * Get status mapping configuration
71
- * @returns Status mapping configuration or undefined
72
- */
73
- getStatusMapping(): Record<string, string> | undefined;
74
- /**
75
- * @param {TestResultType} result
76
- */
77
- addTestResult(result: TestResultType): Promise<void>;
78
- /**
79
- * @param {TestResultType} result
80
- * @private
81
- */
82
- private shouldFilterResult;
83
- /**
84
- * @param {TestResultType} result
85
- * @private
86
- */
87
- private addTestResultToFallback;
88
- /**
89
- * @returns {boolean}
90
- */
91
- isCaptureLogs(): boolean;
92
- /**
93
- * @returns {Promise<void>}
94
- */
44
+ sendResults(): Promise<void>;
95
45
  publish(): Promise<void>;
96
- /**
97
- * @returns {Promise<void>}
98
- */
99
- private publishFallback;
100
- /**
101
- * @todo implement mode registry
102
- * @param {ModeEnum} mode
103
- * @param {OptionsType} options
104
- * @returns {InternalReporterInterface}
105
- * @private
106
- */
107
- private createReporter;
108
- /**
109
- * @param {TestResultType} test
110
- * @private
111
- */
112
- private logTestItem;
113
- private setWithState;
114
- private maskToken;
115
- private sanitizeOptions;
46
+ complete(): Promise<void>;
47
+ isCaptureLogs(): boolean;
116
48
  }