@qanalyzer/forge-cypress 1.1.1
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/LICENSE +202 -0
- package/README.md +105 -0
- package/dist/__fixtures__/saucedemo-13.json +131 -0
- package/dist/enrich-screenshots.d.ts +6 -0
- package/dist/enrich-screenshots.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/metadata-manager.d.ts +13 -0
- package/dist/metadata-manager.js +19 -0
- package/dist/metadata.d.ts +10 -0
- package/dist/metadata.js +35 -0
- package/dist/mocha.d.ts +24 -0
- package/dist/mocha.js +100 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +89 -0
- package/dist/report-builder.d.ts +23 -0
- package/dist/report-builder.js +72 -0
- package/dist/reporter.d.ts +24 -0
- package/dist/reporter.js +141 -0
- package/dist/resolve-options.d.ts +8 -0
- package/dist/resolve-options.js +18 -0
- package/dist/results-manager.d.ts +11 -0
- package/dist/results-manager.js +66 -0
- package/dist/screenshots-manager.d.ts +25 -0
- package/dist/screenshots-manager.js +110 -0
- package/package.json +64 -0
package/dist/mocha.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Programmatic helpers for Cypress Mocha specs (FR63–FR64).
|
|
4
|
+
* Prefer Jira issue keys in `it('AUTH-101 ...')` titles (FR43).
|
|
5
|
+
*
|
|
6
|
+
* `qa.step()` accepts **synchronous** callbacks only (no async/await) —
|
|
7
|
+
* Cypress command-queue safe.
|
|
8
|
+
*
|
|
9
|
+
* In the browser, helpers forward via `cy.task` (requires `@qanalyzer/forge-cypress/metadata`).
|
|
10
|
+
* Outside Cypress (unit tests), they use an in-process buffer.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.MetadataManager = exports.qa = void 0;
|
|
14
|
+
const metadata_manager_1 = require("./metadata-manager");
|
|
15
|
+
Object.defineProperty(exports, "MetadataManager", { enumerable: true, get: function () { return metadata_manager_1.MetadataManager; } });
|
|
16
|
+
function cyRef() {
|
|
17
|
+
const g = globalThis;
|
|
18
|
+
return g.cy;
|
|
19
|
+
}
|
|
20
|
+
function isThenable(value) {
|
|
21
|
+
return (value !== null &&
|
|
22
|
+
typeof value === 'object' &&
|
|
23
|
+
typeof value.then === 'function');
|
|
24
|
+
}
|
|
25
|
+
function pushLocal(type, body) {
|
|
26
|
+
metadata_manager_1.MetadataManager.push(type, body);
|
|
27
|
+
}
|
|
28
|
+
function pushTask(taskName, body) {
|
|
29
|
+
return cyRef().task(taskName, body);
|
|
30
|
+
}
|
|
31
|
+
exports.qa = {
|
|
32
|
+
title(value) {
|
|
33
|
+
const cy = cyRef();
|
|
34
|
+
if (cy) {
|
|
35
|
+
pushTask('qaTitle', value);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
pushLocal('qa-title', value);
|
|
39
|
+
},
|
|
40
|
+
comment(value) {
|
|
41
|
+
const cy = cyRef();
|
|
42
|
+
if (cy) {
|
|
43
|
+
pushTask('qaComment', value);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
pushLocal('qa-comment', value);
|
|
47
|
+
},
|
|
48
|
+
suite(value) {
|
|
49
|
+
const cy = cyRef();
|
|
50
|
+
if (cy) {
|
|
51
|
+
pushTask('qaSuite', value);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
pushLocal('qa-suite', value);
|
|
55
|
+
},
|
|
56
|
+
parameters(values) {
|
|
57
|
+
const cy = cyRef();
|
|
58
|
+
if (cy) {
|
|
59
|
+
pushTask('qaParameters', values);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
pushLocal('qa-parameters', values);
|
|
63
|
+
},
|
|
64
|
+
ignore() {
|
|
65
|
+
const cy = cyRef();
|
|
66
|
+
if (cy) {
|
|
67
|
+
pushTask('qaIgnore');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
pushLocal('qa-ignore', true);
|
|
71
|
+
},
|
|
72
|
+
step(name, body) {
|
|
73
|
+
const cy = cyRef();
|
|
74
|
+
if (cy) {
|
|
75
|
+
pushTask('qaStepStart', name)
|
|
76
|
+
.then(() => {
|
|
77
|
+
const result = body();
|
|
78
|
+
if (isThenable(result)) {
|
|
79
|
+
throw new Error('qa.step() requires a synchronous callback (no async/await) — FR64');
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.then(() => {
|
|
83
|
+
pushTask('qaStepEnd', { name, status: 'passed' });
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
pushLocal('qa-step', name);
|
|
88
|
+
try {
|
|
89
|
+
const result = body();
|
|
90
|
+
if (isThenable(result)) {
|
|
91
|
+
throw new Error('qa.step() requires a synchronous callback (no async/await) — FR64');
|
|
92
|
+
}
|
|
93
|
+
pushLocal('qa-step-end', { name, status: 'passed' });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
pushLocal('qa-step-failed', { name, status: 'failed' });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cypress Node plugin — register in cypress.config.js setupNodeEvents:
|
|
3
|
+
* require('@qanalyzer/forge-cypress/plugin')(on, config);
|
|
4
|
+
*
|
|
5
|
+
* before:run clears the results bridge; after:run publishes FR41
|
|
6
|
+
* (mode=ingest | file) via @qanalyzer/forge-commons.
|
|
7
|
+
* after:screenshot captures failure still images for Phase 3 upload (FR71).
|
|
8
|
+
*/
|
|
9
|
+
type PluginOn = {
|
|
10
|
+
(event: string, handler: (...args: unknown[]) => unknown): void;
|
|
11
|
+
};
|
|
12
|
+
type PluginConfig = Record<string, unknown> & {
|
|
13
|
+
projectRoot?: string;
|
|
14
|
+
reporterOptions?: Record<string, unknown> | null;
|
|
15
|
+
};
|
|
16
|
+
declare function plugin(on: PluginOn, config: PluginConfig): PluginConfig;
|
|
17
|
+
export = plugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cypress Node plugin — register in cypress.config.js setupNodeEvents:
|
|
4
|
+
* require('@qanalyzer/forge-cypress/plugin')(on, config);
|
|
5
|
+
*
|
|
6
|
+
* before:run clears the results bridge; after:run publishes FR41
|
|
7
|
+
* (mode=ingest | file) via @qanalyzer/forge-commons.
|
|
8
|
+
* after:screenshot captures failure still images for Phase 3 upload (FR71).
|
|
9
|
+
*/
|
|
10
|
+
const forge_commons_1 = require("@qanalyzer/forge-commons");
|
|
11
|
+
const enrich_screenshots_1 = require("./enrich-screenshots");
|
|
12
|
+
const report_builder_1 = require("./report-builder");
|
|
13
|
+
const resolve_options_1 = require("./resolve-options");
|
|
14
|
+
const results_manager_1 = require("./results-manager");
|
|
15
|
+
const screenshots_manager_1 = require("./screenshots-manager");
|
|
16
|
+
async function publishCollected(options) {
|
|
17
|
+
const path = results_manager_1.ResultsManager.resolvePath(options.resultsPath);
|
|
18
|
+
const specs = results_manager_1.ResultsManager.getSpecs(path);
|
|
19
|
+
if (specs.length === 0) {
|
|
20
|
+
screenshots_manager_1.ScreenshotsManager.clear();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
forge_commons_1.QAnalyzerReporter.resetInstance();
|
|
24
|
+
const reporter = forge_commons_1.QAnalyzerReporter.getInstance({
|
|
25
|
+
...options,
|
|
26
|
+
mode: options.mode ?? forge_commons_1.ModeEnum.off,
|
|
27
|
+
});
|
|
28
|
+
const mode = reporter.getConfig().mode ?? forge_commons_1.ModeEnum.off;
|
|
29
|
+
if (mode === forge_commons_1.ModeEnum.off) {
|
|
30
|
+
results_manager_1.ResultsManager.clear(path);
|
|
31
|
+
screenshots_manager_1.ScreenshotsManager.clear();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await (0, enrich_screenshots_1.enrichSpecsWithFailureScreenshots)(specs);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Never fail the Cypress run because of attach errors
|
|
39
|
+
}
|
|
40
|
+
const report = (0, report_builder_1.toJestJsonReport)(specs, Date.now());
|
|
41
|
+
await reporter.publishReport(report, {
|
|
42
|
+
format: 'jest-json',
|
|
43
|
+
launchName: options.launchName,
|
|
44
|
+
projectKey: options.projectKey,
|
|
45
|
+
});
|
|
46
|
+
results_manager_1.ResultsManager.clear(path);
|
|
47
|
+
screenshots_manager_1.ScreenshotsManager.clear();
|
|
48
|
+
}
|
|
49
|
+
function plugin(on, config) {
|
|
50
|
+
const qaOptions = (0, resolve_options_1.resolveQaOptions)(config.reporterOptions);
|
|
51
|
+
if (qaOptions.resultsPath) {
|
|
52
|
+
process.env.QANALYZER_CYPRESS_RESULTS_PATH = qaOptions.resultsPath;
|
|
53
|
+
}
|
|
54
|
+
else if (config.projectRoot && !process.env.QANALYZER_CYPRESS_RESULTS_PATH) {
|
|
55
|
+
process.env.QANALYZER_CYPRESS_RESULTS_PATH = `${config.projectRoot}/.qa-cypress-results.json`;
|
|
56
|
+
}
|
|
57
|
+
on('before:run', () => {
|
|
58
|
+
results_manager_1.ResultsManager.clear(results_manager_1.ResultsManager.resolvePath(qaOptions.resultsPath));
|
|
59
|
+
screenshots_manager_1.ScreenshotsManager.clear();
|
|
60
|
+
});
|
|
61
|
+
on('after:screenshot', (details) => {
|
|
62
|
+
try {
|
|
63
|
+
const d = details;
|
|
64
|
+
if (!d?.path)
|
|
65
|
+
return details;
|
|
66
|
+
screenshots_manager_1.ScreenshotsManager.append({
|
|
67
|
+
path: d.path,
|
|
68
|
+
name: d.name,
|
|
69
|
+
specName: d.specName,
|
|
70
|
+
testFailure: d.testFailure,
|
|
71
|
+
takenAt: d.takenAt,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Never fail the Cypress run
|
|
76
|
+
}
|
|
77
|
+
return details;
|
|
78
|
+
});
|
|
79
|
+
on('after:run', async () => {
|
|
80
|
+
try {
|
|
81
|
+
await publishCollected((0, resolve_options_1.resolveQaOptions)(config.reporterOptions));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Never fail the Cypress run because of publish errors
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { JestVitestJsonReport, QaMetaWire } from '@qanalyzer/forge-commons';
|
|
2
|
+
export type CypressAssertionInput = {
|
|
3
|
+
ancestorTitles: string[];
|
|
4
|
+
title: string;
|
|
5
|
+
fullName?: string;
|
|
6
|
+
status: 'passed' | 'failed' | 'pending' | 'todo';
|
|
7
|
+
duration?: number;
|
|
8
|
+
failureMessages?: string[];
|
|
9
|
+
meta?: {
|
|
10
|
+
qa?: QaMetaWire;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type CypressSpecInput = {
|
|
14
|
+
/** Spec path (e.g. cypress/e2e/login.cy.js) */
|
|
15
|
+
name: string;
|
|
16
|
+
startTime?: number;
|
|
17
|
+
endTime?: number;
|
|
18
|
+
assertions: CypressAssertionInput[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Normalize collected Cypress/Mocha specs into FR41 jest-json shape A.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toJestJsonReport(specs: readonly CypressSpecInput[], startTime?: number): JestVitestJsonReport;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toJestJsonReport = toJestJsonReport;
|
|
4
|
+
function mapAssertion(a) {
|
|
5
|
+
const fullName = a.fullName ??
|
|
6
|
+
(a.ancestorTitles.length > 0
|
|
7
|
+
? `${a.ancestorTitles.join(' ')} ${a.title}`
|
|
8
|
+
: a.title);
|
|
9
|
+
const assertion = {
|
|
10
|
+
ancestorTitles: a.ancestorTitles,
|
|
11
|
+
fullName,
|
|
12
|
+
title: a.title,
|
|
13
|
+
status: a.status,
|
|
14
|
+
duration: a.duration,
|
|
15
|
+
failureMessages: a.failureMessages ?? [],
|
|
16
|
+
};
|
|
17
|
+
if (a.meta?.qa) {
|
|
18
|
+
assertion.meta = { qa: a.meta.qa };
|
|
19
|
+
}
|
|
20
|
+
return assertion;
|
|
21
|
+
}
|
|
22
|
+
function fileStatus(assertions) {
|
|
23
|
+
return assertions.some((a) => a.status === 'failed') ? 'failed' : 'passed';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize collected Cypress/Mocha specs into FR41 jest-json shape A.
|
|
27
|
+
*/
|
|
28
|
+
function toJestJsonReport(specs, startTime) {
|
|
29
|
+
const testResults = specs.map((spec) => {
|
|
30
|
+
const assertionResults = spec.assertions.map(mapAssertion);
|
|
31
|
+
return {
|
|
32
|
+
name: spec.name,
|
|
33
|
+
status: fileStatus(assertionResults),
|
|
34
|
+
startTime: spec.startTime,
|
|
35
|
+
endTime: spec.endTime,
|
|
36
|
+
assertionResults,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
let numPassedTests = 0;
|
|
40
|
+
let numFailedTests = 0;
|
|
41
|
+
let numPendingTests = 0;
|
|
42
|
+
let numTodoTests = 0;
|
|
43
|
+
for (const file of testResults) {
|
|
44
|
+
for (const a of file.assertionResults ?? []) {
|
|
45
|
+
if (a.status === 'passed')
|
|
46
|
+
numPassedTests += 1;
|
|
47
|
+
else if (a.status === 'failed')
|
|
48
|
+
numFailedTests += 1;
|
|
49
|
+
else if (a.status === 'todo')
|
|
50
|
+
numTodoTests += 1;
|
|
51
|
+
else
|
|
52
|
+
numPendingTests += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const numTotalTests = numPassedTests + numFailedTests + numPendingTests + numTodoTests;
|
|
56
|
+
const numFailedTestSuites = testResults.filter((t) => t.status === 'failed').length;
|
|
57
|
+
const numPassedTestSuites = testResults.length - numFailedTestSuites;
|
|
58
|
+
return {
|
|
59
|
+
numTotalTestSuites: testResults.length,
|
|
60
|
+
numPassedTestSuites,
|
|
61
|
+
numFailedTestSuites,
|
|
62
|
+
numPendingTestSuites: 0,
|
|
63
|
+
numTotalTests,
|
|
64
|
+
numPassedTests,
|
|
65
|
+
numFailedTests,
|
|
66
|
+
numPendingTests,
|
|
67
|
+
numTodoTests,
|
|
68
|
+
startTime,
|
|
69
|
+
success: numFailedTests === 0,
|
|
70
|
+
testResults,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { reporters, Runner, type MochaOptions } from 'mocha';
|
|
2
|
+
import { type OptionsType } from '@qanalyzer/forge-commons';
|
|
3
|
+
export type CypressQaOptions = OptionsType & {
|
|
4
|
+
/** Override results bridge path (also QANALYZER_CYPRESS_RESULTS_PATH). */
|
|
5
|
+
resultsPath?: string;
|
|
6
|
+
};
|
|
7
|
+
export type CypressQaReporterOptions = Omit<MochaOptions, 'reporterOptions'> & {
|
|
8
|
+
reporterOptions?: CypressQaOptions;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Cypress Mocha reporter for QAnalyzer.
|
|
12
|
+
*
|
|
13
|
+
* Collects per-`it()` results + `qa.*` metadata, appends to ResultsManager.
|
|
14
|
+
* Plugin `after:run` publishes FR41 (mode=ingest|file). mode=off no-ops.
|
|
15
|
+
*/
|
|
16
|
+
export declare class CypressQaReporter extends reporters.Base {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly byFile;
|
|
19
|
+
private readonly runStart;
|
|
20
|
+
constructor(runner: Runner, options?: CypressQaReporterOptions);
|
|
21
|
+
private record;
|
|
22
|
+
private flushToResultsManager;
|
|
23
|
+
}
|
|
24
|
+
export default CypressQaReporter;
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CypressQaReporter = void 0;
|
|
4
|
+
const mocha_1 = require("mocha");
|
|
5
|
+
const forge_commons_1 = require("@qanalyzer/forge-commons");
|
|
6
|
+
const metadata_manager_1 = require("./metadata-manager");
|
|
7
|
+
const results_manager_1 = require("./results-manager");
|
|
8
|
+
function mapStatus(state) {
|
|
9
|
+
if (state === 'failed')
|
|
10
|
+
return 'failed';
|
|
11
|
+
if (state === 'pending')
|
|
12
|
+
return 'pending';
|
|
13
|
+
return 'passed';
|
|
14
|
+
}
|
|
15
|
+
function ancestorTitles(test) {
|
|
16
|
+
try {
|
|
17
|
+
const path = typeof test.titlePath === 'function' ? test.titlePath() : [];
|
|
18
|
+
if (path.length > 1)
|
|
19
|
+
return path.slice(0, -1);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// fall through
|
|
23
|
+
}
|
|
24
|
+
const titles = [];
|
|
25
|
+
let parent = test.parent;
|
|
26
|
+
while (parent && !parent.root) {
|
|
27
|
+
if (parent.title)
|
|
28
|
+
titles.unshift(parent.title);
|
|
29
|
+
parent = parent.parent;
|
|
30
|
+
}
|
|
31
|
+
return titles;
|
|
32
|
+
}
|
|
33
|
+
function specFileName(test) {
|
|
34
|
+
const fromTest = test.file;
|
|
35
|
+
if (fromTest)
|
|
36
|
+
return fromTest;
|
|
37
|
+
let parent = test.parent;
|
|
38
|
+
while (parent) {
|
|
39
|
+
const file = parent.file;
|
|
40
|
+
if (file)
|
|
41
|
+
return file;
|
|
42
|
+
parent = parent.parent;
|
|
43
|
+
}
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
function failureMessages(test) {
|
|
47
|
+
const err = test.err;
|
|
48
|
+
if (!err)
|
|
49
|
+
return [];
|
|
50
|
+
const msg = err.stack || err.message;
|
|
51
|
+
return msg ? [msg] : [];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Cypress Mocha reporter for QAnalyzer.
|
|
55
|
+
*
|
|
56
|
+
* Collects per-`it()` results + `qa.*` metadata, appends to ResultsManager.
|
|
57
|
+
* Plugin `after:run` publishes FR41 (mode=ingest|file). mode=off no-ops.
|
|
58
|
+
*/
|
|
59
|
+
class CypressQaReporter extends mocha_1.reporters.Base {
|
|
60
|
+
options;
|
|
61
|
+
byFile = new Map();
|
|
62
|
+
runStart = Date.now();
|
|
63
|
+
constructor(runner, options = {}) {
|
|
64
|
+
super(runner, options);
|
|
65
|
+
this.options = options.reporterOptions ?? {};
|
|
66
|
+
if (this.options.resultsPath) {
|
|
67
|
+
process.env.QANALYZER_CYPRESS_RESULTS_PATH = this.options.resultsPath;
|
|
68
|
+
}
|
|
69
|
+
runner.on(mocha_1.Runner.constants.EVENT_TEST_BEGIN, () => {
|
|
70
|
+
metadata_manager_1.MetadataManager.clear();
|
|
71
|
+
});
|
|
72
|
+
runner.on(mocha_1.Runner.constants.EVENT_TEST_PASS, (test) => {
|
|
73
|
+
this.record(test, 'passed');
|
|
74
|
+
});
|
|
75
|
+
runner.on(mocha_1.Runner.constants.EVENT_TEST_FAIL, (test) => {
|
|
76
|
+
this.record(test, 'failed');
|
|
77
|
+
});
|
|
78
|
+
runner.on(mocha_1.Runner.constants.EVENT_TEST_PENDING, (test) => {
|
|
79
|
+
this.record(test, 'pending');
|
|
80
|
+
});
|
|
81
|
+
runner.once(mocha_1.Runner.constants.EVENT_RUN_END, () => {
|
|
82
|
+
this.flushToResultsManager();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
record(test, state) {
|
|
86
|
+
try {
|
|
87
|
+
const entries = metadata_manager_1.MetadataManager.getEntries();
|
|
88
|
+
const wire = (0, forge_commons_1.qaMetaFromEntries)(entries, {
|
|
89
|
+
framework: 'cypress',
|
|
90
|
+
reporter: '@qanalyzer/forge-cypress',
|
|
91
|
+
});
|
|
92
|
+
metadata_manager_1.MetadataManager.clear();
|
|
93
|
+
const ancestors = ancestorTitles(test);
|
|
94
|
+
const assertion = {
|
|
95
|
+
ancestorTitles: ancestors,
|
|
96
|
+
title: test.title,
|
|
97
|
+
fullName: ancestors.length > 0 ? `${ancestors.join(' ')} ${test.title}` : test.title,
|
|
98
|
+
status: mapStatus(state ?? test.state),
|
|
99
|
+
duration: test.duration ?? undefined,
|
|
100
|
+
failureMessages: state === 'failed' ? failureMessages(test) : [],
|
|
101
|
+
};
|
|
102
|
+
if (wire) {
|
|
103
|
+
assertion.meta = { qa: wire };
|
|
104
|
+
}
|
|
105
|
+
const file = specFileName(test);
|
|
106
|
+
const list = this.byFile.get(file) ?? [];
|
|
107
|
+
list.push(assertion);
|
|
108
|
+
this.byFile.set(file, list);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Never fail the Cypress run
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
flushToResultsManager() {
|
|
115
|
+
try {
|
|
116
|
+
const mode = this.options.mode ?? forge_commons_1.ModeEnum.off;
|
|
117
|
+
// Always buffer when not off so after:run can publish; also buffer when
|
|
118
|
+
// off so local debugging of the bridge still works without publish.
|
|
119
|
+
const path = results_manager_1.ResultsManager.resolvePath(this.options.resultsPath);
|
|
120
|
+
for (const [name, assertions] of this.byFile) {
|
|
121
|
+
if (assertions.length === 0)
|
|
122
|
+
continue;
|
|
123
|
+
const spec = {
|
|
124
|
+
name,
|
|
125
|
+
startTime: this.runStart,
|
|
126
|
+
endTime: Date.now(),
|
|
127
|
+
assertions,
|
|
128
|
+
};
|
|
129
|
+
results_manager_1.ResultsManager.appendSpec(spec, path);
|
|
130
|
+
}
|
|
131
|
+
this.byFile.clear();
|
|
132
|
+
// Hint for logs / tests — actual publish is plugin after:run
|
|
133
|
+
void mode;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Never fail the Cypress run
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.CypressQaReporter = CypressQaReporter;
|
|
141
|
+
exports.default = CypressQaReporter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OptionsType } from '@qanalyzer/forge-commons';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve @qanalyzer/forge-cypress options from Cypress reporterOptions.
|
|
4
|
+
* Supports cypress-multi-reporters wrapper and direct reporter config.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveQaOptions(reporterOptions: unknown): OptionsType & {
|
|
7
|
+
resultsPath?: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveQaOptions = resolveQaOptions;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve @qanalyzer/forge-cypress options from Cypress reporterOptions.
|
|
6
|
+
* Supports cypress-multi-reporters wrapper and direct reporter config.
|
|
7
|
+
*/
|
|
8
|
+
function resolveQaOptions(reporterOptions) {
|
|
9
|
+
if (reporterOptions == null || typeof reporterOptions !== 'object') {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
const record = reporterOptions;
|
|
13
|
+
const wrapped = record.qaCypressReporterOptions;
|
|
14
|
+
if (wrapped !== undefined && typeof wrapped === 'object' && wrapped !== null) {
|
|
15
|
+
return wrapped;
|
|
16
|
+
}
|
|
17
|
+
return record;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CypressSpecInput } from './report-builder';
|
|
2
|
+
/**
|
|
3
|
+
* File bridge across Cypress specs (reporter may run once per spec).
|
|
4
|
+
* Plugin `before:run` clears; reporter appends; `after:run` reads + publishes.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ResultsManager {
|
|
7
|
+
static resolvePath(explicit?: string): string;
|
|
8
|
+
static clear(path?: string): void;
|
|
9
|
+
static getSpecs(path?: string): CypressSpecInput[];
|
|
10
|
+
static appendSpec(spec: CypressSpecInput, path?: string): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResultsManager = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const DEFAULT_BASENAME = 'qa-cypress-results.json';
|
|
8
|
+
/**
|
|
9
|
+
* File bridge across Cypress specs (reporter may run once per spec).
|
|
10
|
+
* Plugin `before:run` clears; reporter appends; `after:run` reads + publishes.
|
|
11
|
+
*/
|
|
12
|
+
class ResultsManager {
|
|
13
|
+
static resolvePath(explicit) {
|
|
14
|
+
if (explicit)
|
|
15
|
+
return explicit;
|
|
16
|
+
if (process.env.QANALYZER_CYPRESS_RESULTS_PATH) {
|
|
17
|
+
return process.env.QANALYZER_CYPRESS_RESULTS_PATH;
|
|
18
|
+
}
|
|
19
|
+
return (0, node_path_1.join)((0, node_os_1.tmpdir)(), DEFAULT_BASENAME);
|
|
20
|
+
}
|
|
21
|
+
static clear(path = ResultsManager.resolvePath()) {
|
|
22
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
23
|
+
try {
|
|
24
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
static getSpecs(path = ResultsManager.resolvePath()) {
|
|
32
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
33
|
+
return [];
|
|
34
|
+
try {
|
|
35
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
36
|
+
return Array.isArray(raw.specs) ? raw.specs : [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
static appendSpec(spec, path = ResultsManager.resolvePath()) {
|
|
43
|
+
const specs = ResultsManager.getSpecs(path);
|
|
44
|
+
const existing = specs.findIndex((s) => s.name === spec.name);
|
|
45
|
+
if (existing >= 0) {
|
|
46
|
+
specs[existing] = {
|
|
47
|
+
...spec,
|
|
48
|
+
assertions: [
|
|
49
|
+
...(specs[existing]?.assertions ?? []),
|
|
50
|
+
...spec.assertions,
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
specs.push(spec);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
59
|
+
(0, node_fs_1.writeFileSync)(path, JSON.stringify({ specs }, null, 0), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Never fail the Cypress run
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.ResultsManager = ResultsManager;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type CypressScreenshotRecord = {
|
|
2
|
+
path: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
specName?: string;
|
|
5
|
+
testFailure?: boolean;
|
|
6
|
+
takenAt?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class ScreenshotsManager {
|
|
9
|
+
static resolvePath(explicit?: string): string;
|
|
10
|
+
static clear(path?: string): void;
|
|
11
|
+
static getAll(path?: string): CypressScreenshotRecord[];
|
|
12
|
+
static append(record: CypressScreenshotRecord, path?: string): void;
|
|
13
|
+
}
|
|
14
|
+
/** True for still images; videos are skipped (Phase 3 still-image only). */
|
|
15
|
+
export declare function isStillImagePath(filePath: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Match a failure screenshot to a failed assertion by title / fullName.
|
|
18
|
+
*/
|
|
19
|
+
export declare function matchScreenshotToAssertion(shot: CypressScreenshotRecord, assertion: {
|
|
20
|
+
title: string;
|
|
21
|
+
fullName?: string;
|
|
22
|
+
status: string;
|
|
23
|
+
}, specName: string): boolean;
|
|
24
|
+
/** Next unused failure still-image for this spec (ordered fallback). */
|
|
25
|
+
export declare function nextUnusedSpecScreenshot(shots: readonly CypressScreenshotRecord[], used: ReadonlySet<number>, specName: string): number;
|