dceky 1.2.1 → 1.2.5
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/README.md +23 -17
- package/lib/setup/addToGitIgnore.js +2 -0
- package/lib/setup/addToGitIgnore.js.map +1 -1
- package/lib/src/genConfiguration/index.js +0 -9
- package/lib/src/genConfiguration/index.js.map +1 -1
- package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.d.ts +1 -1
- package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.js +1 -1
- package/lib/start/helpers/embedScreenshotsInReportData.d.ts +14 -0
- package/lib/start/helpers/embedScreenshotsInReportData.js +139 -0
- package/lib/start/helpers/embedScreenshotsInReportData.js.map +1 -0
- package/lib/start/helpers/executeCypress.js +1 -1
- package/lib/start/helpers/executeCypress.js.map +1 -1
- package/lib/start/helpers/generateHtmlReport.js +7 -0
- package/lib/start/helpers/generateHtmlReport.js.map +1 -1
- package/lib/start/helpers/getCypressDetectedBrowsersForChooser.d.ts +2 -1
- package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js +34 -22
- package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js.map +1 -1
- package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js +52 -0
- package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js.map +1 -1
- package/lib/start/helpers/reportHomepage.ejs +18 -7
- package/lib/start/index.d.ts +2 -2
- package/lib/start/index.js +214 -175
- package/lib/start/index.js.map +1 -1
- package/package.json +2 -1
- package/setup/addToGitIgnore.ts +2 -0
- package/src/genConfiguration/index.ts +0 -13
- package/start/constants/DEFAULT_THREADS_PER_COMBO.ts +1 -1
- package/start/helpers/embedScreenshotsInReportData.ts +171 -0
- package/start/helpers/executeCypress.ts +1 -1
- package/start/helpers/generateHtmlReport.ts +12 -0
- package/start/helpers/getCypressDetectedBrowsersForChooser.ts +41 -23
- package/start/helpers/mergeAllReportsAndGenerateHtml.ts +60 -0
- package/start/helpers/reportHomepage.ejs +18 -7
- package/start/index.ts +218 -181
- package/start/helpers/extractArgValue.ts +0 -42
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
type Screenshot = {
|
|
6
|
+
href: string;
|
|
7
|
+
matchKey: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize Cypress screenshot names and Mochawesome test titles for matching.
|
|
12
|
+
* @author Yuen Ler Chow
|
|
13
|
+
* @param value raw screenshot filename or test title
|
|
14
|
+
* @returns normalized value used for screenshot/test matching
|
|
15
|
+
*/
|
|
16
|
+
const normalize = (value: string): string => {
|
|
17
|
+
return (
|
|
18
|
+
value
|
|
19
|
+
.replace(/\s+\(failed\)(?:\s+\(attempt\s+\d+\))?$/i, '')
|
|
20
|
+
.replace(/\s+\(attempt\s+\d+\)$/i, '')
|
|
21
|
+
.replace(/[^a-z0-9]+/gi, '')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively collect screenshots and store paths relative to the HTML report.
|
|
28
|
+
* @author Yuen Ler Chow
|
|
29
|
+
* @param dir screenshot directory to scan
|
|
30
|
+
* @param reportDir directory where marge will generate the HTML report
|
|
31
|
+
* @returns screenshot metadata used for report embedding
|
|
32
|
+
*/
|
|
33
|
+
const collectScreenshots = (dir: string, reportDir: string): Screenshot[] => {
|
|
34
|
+
if (!fs.existsSync(dir)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
39
|
+
const entryPath = path.join(dir, entry.name);
|
|
40
|
+
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
return collectScreenshots(entryPath, reportDir);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!entry.isFile() || !entry.name.endsWith('.png')) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [{
|
|
50
|
+
href: path.relative(reportDir, entryPath).replace(/\\/g, '/'),
|
|
51
|
+
matchKey: normalize(entry.name.replace(/\.png$/i, '')),
|
|
52
|
+
}];
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert Mochawesome's context field into an array while preserving existing entries.
|
|
58
|
+
* @author Yuen Ler Chow
|
|
59
|
+
* @param context raw Mochawesome context value
|
|
60
|
+
* @returns context entries as an array
|
|
61
|
+
*/
|
|
62
|
+
const getContextItems = (context: any): any[] => {
|
|
63
|
+
if (!context) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(context)) {
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof context !== 'string') {
|
|
72
|
+
return [context];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(context);
|
|
77
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
78
|
+
} catch {
|
|
79
|
+
return [context];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Walk a Mochawesome report node and attach matching screenshots to failed tests.
|
|
85
|
+
* @author Yuen Ler Chow
|
|
86
|
+
* @param node Mochawesome report node, suite, test, or array of nodes
|
|
87
|
+
* @param screenshots screenshots available for matching
|
|
88
|
+
* @returns number of failed tests that received screenshot context
|
|
89
|
+
*/
|
|
90
|
+
const embedScreenshotsInNode = (node: any, screenshots: Screenshot[]): number => {
|
|
91
|
+
if (!node || typeof node !== 'object') {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(node)) {
|
|
96
|
+
return node.reduce((sum, child) => {
|
|
97
|
+
return sum + embedScreenshotsInNode(child, screenshots);
|
|
98
|
+
}, 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let numEmbedded = 0;
|
|
102
|
+
if (node.fail) {
|
|
103
|
+
const testKey = normalize(node.fullTitle || node.title || '');
|
|
104
|
+
const matches = screenshots.filter((screenshot) => {
|
|
105
|
+
return screenshot.matchKey.endsWith(testKey);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (matches.length > 0) {
|
|
109
|
+
const contextItems = getContextItems(node.context);
|
|
110
|
+
const existingHrefs = new Set(
|
|
111
|
+
contextItems.map((item) => {
|
|
112
|
+
return typeof item === 'string' ? item : item.value;
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
matches.forEach((screenshot, index) => {
|
|
117
|
+
if (!existingHrefs.has(screenshot.href)) {
|
|
118
|
+
contextItems.push({
|
|
119
|
+
title: matches.length === 1 ? 'Screenshot' : `Screenshot ${index + 1}`,
|
|
120
|
+
value: screenshot.href,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line no-param-reassign
|
|
126
|
+
node.context = JSON.stringify(contextItems.length === 1 ? contextItems[0] : contextItems, null, 2);
|
|
127
|
+
numEmbedded += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
numEmbedded
|
|
133
|
+
+ embedScreenshotsInNode(node.suites, screenshots)
|
|
134
|
+
+ embedScreenshotsInNode(node.tests, screenshots)
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Add screenshot context to failed tests in a merged Mochawesome JSON report.
|
|
140
|
+
* @author Yuen Ler Chow
|
|
141
|
+
* @param opts object of arguments
|
|
142
|
+
* @param opts.reportDataPath path to report-data.json
|
|
143
|
+
* @param opts.screenshotsDir path to Cypress screenshots for this report
|
|
144
|
+
* @param opts.reportDir path where marge will generate the HTML report
|
|
145
|
+
*/
|
|
146
|
+
const embedScreenshotsInReportData = (
|
|
147
|
+
opts: {
|
|
148
|
+
reportDataPath: string,
|
|
149
|
+
screenshotsDir: string,
|
|
150
|
+
reportDir: string,
|
|
151
|
+
},
|
|
152
|
+
): void => {
|
|
153
|
+
const { reportDataPath, screenshotsDir, reportDir } = opts;
|
|
154
|
+
const screenshots = collectScreenshots(screenshotsDir, reportDir);
|
|
155
|
+
if (!fs.existsSync(reportDataPath) || screenshots.length === 0) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const report = JSON.parse(fs.readFileSync(reportDataPath, 'utf8'));
|
|
160
|
+
const numEmbedded = embedScreenshotsInNode(report.results, screenshots);
|
|
161
|
+
|
|
162
|
+
if (numEmbedded === 0) {
|
|
163
|
+
console.warn('⚠️ Screenshots were found, but none matched failed tests in the Mochawesome report.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(reportDataPath, JSON.stringify(report, null, 2));
|
|
168
|
+
console.log(` ✅ Embedded screenshots into ${numEmbedded} failed test(s)`);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default embedScreenshotsInReportData;
|
|
@@ -44,7 +44,7 @@ const executeCypress = async (
|
|
|
44
44
|
console.log(`Headless: ${isHeadless}`);
|
|
45
45
|
if (isHeadless) {
|
|
46
46
|
console.log(`E2E test folder: ${e2eTestFolder}`);
|
|
47
|
-
console.log(`
|
|
47
|
+
console.log(`Num Test Files in Parallel Per Combo: ${threadsPerCombo}`);
|
|
48
48
|
}
|
|
49
49
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
50
50
|
|
|
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import getRootPath from './getRootPath';
|
|
6
|
+
import embedScreenshotsInReportData from './embedScreenshotsInReportData';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generate HTML report from merged JSON report using mochawesome-report-generator (marge)
|
|
@@ -31,6 +32,11 @@ const generateHtmlReport = (
|
|
|
31
32
|
`${profileName}-${browserName}`,
|
|
32
33
|
'report',
|
|
33
34
|
);
|
|
35
|
+
const screenshotsDir = path.join(
|
|
36
|
+
resultsDir,
|
|
37
|
+
`${profileName}-${browserName}`,
|
|
38
|
+
'screenshots',
|
|
39
|
+
);
|
|
34
40
|
const expectedHtmlPath = path.join(reportDir, 'report-data.html');
|
|
35
41
|
|
|
36
42
|
console.log(`\n📄 Generate HTML Report Debug for ${profileName} + ${browserName}:`);
|
|
@@ -44,6 +50,12 @@ const generateHtmlReport = (
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
try {
|
|
53
|
+
embedScreenshotsInReportData({
|
|
54
|
+
reportDataPath,
|
|
55
|
+
screenshotsDir,
|
|
56
|
+
reportDir,
|
|
57
|
+
});
|
|
58
|
+
|
|
47
59
|
const command = `npx marge "${reportDataPath}" --reportDir "${reportDir}"`;
|
|
48
60
|
|
|
49
61
|
console.log(` Running: ${command}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
3
|
|
|
4
4
|
// Import helpers
|
|
5
5
|
import getRootPath from './getRootPath';
|
|
@@ -12,33 +12,51 @@ import AVAILABLE_BROWSERS from '../constants/AVAILABLE_BROWSERS';
|
|
|
12
12
|
* `npx cypress info` reports (`- Name:` lines), plus Webkit (always shown
|
|
13
13
|
* because `cypress info` omits WebKit; https://github.com/cypress-io/cypress/issues/27304).
|
|
14
14
|
* @author Yuen Ler Chow
|
|
15
|
+
* @returns promise containing browsers to show in the chooser
|
|
15
16
|
*/
|
|
16
|
-
const getCypressDetectedBrowsersForChooser = (): typeof AVAILABLE_BROWSERS => {
|
|
17
|
+
const getCypressDetectedBrowsersForChooser = (): Promise<typeof AVAILABLE_BROWSERS> => {
|
|
17
18
|
const root = getRootPath();
|
|
18
|
-
const result = spawnSync('npx', ['cypress', 'info'], {
|
|
19
|
-
cwd: root,
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
shell: true,
|
|
22
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
23
|
-
});
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const detected = new Set<string>();
|
|
22
|
+
let output = '';
|
|
23
|
+
const cypressInfo = spawn('npx', ['cypress', 'info'], {
|
|
24
|
+
cwd: root,
|
|
25
|
+
shell: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
cypressInfo.stdout.on('data', (chunk) => {
|
|
29
|
+
output += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
cypressInfo.stderr.on('data', (chunk) => {
|
|
32
|
+
output += chunk.toString();
|
|
34
33
|
});
|
|
35
|
-
} else {
|
|
36
|
-
console.warn('Could not detect installed browsers (cypress info failed).');
|
|
37
|
-
}
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
cypressInfo.on('error', () => {
|
|
36
|
+
console.warn('Could not detect installed browsers (cypress info failed).');
|
|
37
|
+
resolve([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
cypressInfo.on('close', (code) => {
|
|
41
|
+
if (code !== 0) {
|
|
42
|
+
console.warn('Could not detect installed browsers (cypress info failed).');
|
|
43
|
+
} else {
|
|
44
|
+
output.split('\n').forEach((line) => {
|
|
45
|
+
const match = line.match(/^\s*-\s*Name:\s*(\S+)/);
|
|
46
|
+
if (match) {
|
|
47
|
+
const [, name] = match;
|
|
48
|
+
detected.add(name.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resolve(
|
|
54
|
+
AVAILABLE_BROWSERS.filter((b) => {
|
|
55
|
+
// Always include Webkit in the chooser because "npx cypress info" omits WebKit
|
|
56
|
+
return b.name === 'webkit' || detected.has(b.name);
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
42
60
|
});
|
|
43
61
|
};
|
|
44
62
|
|
|
@@ -50,6 +50,64 @@ const annotateMochawesomeSuites = (node: any, label: string): void => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Rewrite screenshot paths so they resolve from the combined all-runs.html report.
|
|
55
|
+
* @author Yuen Ler Chow
|
|
56
|
+
* @param node Mochawesome report node, suite, test, or array of nodes
|
|
57
|
+
* @param comboFolderName folder name for the profile+browser run
|
|
58
|
+
*/
|
|
59
|
+
const rewriteScreenshotContextPaths = (node: any, comboFolderName: string): void => {
|
|
60
|
+
if (!node || typeof node !== 'object') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(node)) {
|
|
65
|
+
node.forEach((child: any) => {
|
|
66
|
+
rewriteScreenshotContextPaths(child, comboFolderName);
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof node.context === 'string') {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(node.context);
|
|
74
|
+
const contextItems = Array.isArray(parsed) ? parsed : [parsed];
|
|
75
|
+
let didRewrite = false;
|
|
76
|
+
|
|
77
|
+
contextItems.forEach((context) => {
|
|
78
|
+
if (
|
|
79
|
+
typeof context !== 'string'
|
|
80
|
+
&& typeof context.value === 'string'
|
|
81
|
+
&& context.value.startsWith('../screenshots/')
|
|
82
|
+
) {
|
|
83
|
+
// eslint-disable-next-line no-param-reassign
|
|
84
|
+
context.value = `${comboFolderName}/${context.value.replace(/^\.\.\//, '')}`;
|
|
85
|
+
didRewrite = true;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (didRewrite) {
|
|
90
|
+
// eslint-disable-next-line no-param-reassign
|
|
91
|
+
node.context = JSON.stringify(Array.isArray(parsed) ? contextItems : contextItems[0], null, 2);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore non-JSON contexts generated outside of ky.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(node.suites)) {
|
|
99
|
+
node.suites.forEach((suite: any) => {
|
|
100
|
+
rewriteScreenshotContextPaths(suite, comboFolderName);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(node.tests)) {
|
|
105
|
+
node.tests.forEach((test: any) => {
|
|
106
|
+
rewriteScreenshotContextPaths(test, comboFolderName);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
53
111
|
/**
|
|
54
112
|
* Merge all per-combination mochawesome JSON reports into a single JSON
|
|
55
113
|
* and generate one combined HTML report for all runs.
|
|
@@ -80,12 +138,14 @@ const mergeAllReportsAndGenerateHtml = (
|
|
|
80
138
|
const raw = fs.readFileSync(jsonPath, 'utf-8');
|
|
81
139
|
const data = JSON.parse(raw);
|
|
82
140
|
const label = `[${result.profileName}][${result.browser}] `;
|
|
141
|
+
const comboFolderName = `${result.profileName}-${result.browser}`;
|
|
83
142
|
|
|
84
143
|
if (Array.isArray(data.results)) {
|
|
85
144
|
data.results.forEach((res: any) => {
|
|
86
145
|
if (res && res.suites) {
|
|
87
146
|
annotateMochawesomeSuites(res.suites, label);
|
|
88
147
|
}
|
|
148
|
+
rewriteScreenshotContextPaths(res, comboFolderName);
|
|
89
149
|
});
|
|
90
150
|
}
|
|
91
151
|
|
|
@@ -179,11 +179,20 @@
|
|
|
179
179
|
background: #115293;
|
|
180
180
|
border-color: #115293;
|
|
181
181
|
}
|
|
182
|
-
.screenshots-
|
|
182
|
+
.screenshots-details {
|
|
183
183
|
margin-top: 0.875rem;
|
|
184
|
+
border-top: 0.0625rem solid #e5e5e5;
|
|
185
|
+
padding-top: 0.75rem;
|
|
186
|
+
}
|
|
187
|
+
.screenshots-details summary {
|
|
188
|
+
cursor: pointer;
|
|
184
189
|
font-weight: 600;
|
|
190
|
+
color: #333;
|
|
191
|
+
list-style-position: inside;
|
|
185
192
|
}
|
|
186
193
|
.screenshot-list {
|
|
194
|
+
margin-top: 0.625rem;
|
|
195
|
+
margin-bottom: 0;
|
|
187
196
|
padding-left: 1.25rem;
|
|
188
197
|
padding-right: 0.5rem;
|
|
189
198
|
}
|
|
@@ -265,12 +274,14 @@
|
|
|
265
274
|
<p class="no-report">Report not available</p>
|
|
266
275
|
<% } %>
|
|
267
276
|
<% if (report.screenshots && report.screenshots.length > 0) { %>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
<details class="screenshots-details">
|
|
278
|
+
<summary>Screenshots from failed tests (<%= report.screenshots.length %>)</summary>
|
|
279
|
+
<ul class="screenshot-list">
|
|
280
|
+
<% report.screenshots.forEach((screenshot) => { %>
|
|
281
|
+
<li><a href="<%= screenshot.href %>" target="_blank"><%= screenshot.name %></a></li>
|
|
282
|
+
<% }) %>
|
|
283
|
+
</ul>
|
|
284
|
+
</details>
|
|
274
285
|
<% } %>
|
|
275
286
|
</div>
|
|
276
287
|
<% }) %>
|