dceky 1.2.1 → 1.2.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.
Files changed (32) hide show
  1. package/README.md +23 -17
  2. package/lib/src/genConfiguration/index.js +0 -9
  3. package/lib/src/genConfiguration/index.js.map +1 -1
  4. package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.d.ts +1 -1
  5. package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.js +1 -1
  6. package/lib/start/helpers/embedScreenshotsInReportData.d.ts +14 -0
  7. package/lib/start/helpers/embedScreenshotsInReportData.js +139 -0
  8. package/lib/start/helpers/embedScreenshotsInReportData.js.map +1 -0
  9. package/lib/start/helpers/executeCypress.js +1 -1
  10. package/lib/start/helpers/executeCypress.js.map +1 -1
  11. package/lib/start/helpers/generateHtmlReport.js +7 -0
  12. package/lib/start/helpers/generateHtmlReport.js.map +1 -1
  13. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.d.ts +2 -1
  14. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js +34 -22
  15. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js.map +1 -1
  16. package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js +52 -0
  17. package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js.map +1 -1
  18. package/lib/start/helpers/reportHomepage.ejs +18 -7
  19. package/lib/start/index.d.ts +2 -2
  20. package/lib/start/index.js +214 -175
  21. package/lib/start/index.js.map +1 -1
  22. package/package.json +2 -1
  23. package/src/genConfiguration/index.ts +0 -13
  24. package/start/constants/DEFAULT_THREADS_PER_COMBO.ts +1 -1
  25. package/start/helpers/embedScreenshotsInReportData.ts +171 -0
  26. package/start/helpers/executeCypress.ts +1 -1
  27. package/start/helpers/generateHtmlReport.ts +12 -0
  28. package/start/helpers/getCypressDetectedBrowsersForChooser.ts +41 -23
  29. package/start/helpers/mergeAllReportsAndGenerateHtml.ts +60 -0
  30. package/start/helpers/reportHomepage.ejs +18 -7
  31. package/start/index.ts +218 -181
  32. 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(`Threads per combo: ${threadsPerCombo}`);
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 { spawnSync } from 'child_process';
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
- const detected = new Set<string>();
26
- if (!result.error && result.status === 0) {
27
- const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
28
- text.split('\n').forEach((line) => {
29
- const m = line.match(/^\s*-\s*Name:\s*(\S+)/);
30
- if (m) {
31
- const [, name] = m;
32
- detected.add(name.toLowerCase());
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
- return AVAILABLE_BROWSERS.filter((b) => {
40
- // Always include Webkit in the chooser because "npx cypress info" omits WebKit
41
- return b.name === 'webkit' || detected.has(b.name);
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-header {
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
- <div class="screenshots-header">📸 Screenshots from failed tests</div>
269
- <ul class="screenshot-list">
270
- <% report.screenshots.forEach((screenshot) => { %>
271
- <li><a href="<%= screenshot.href %>" target="_blank"><%= screenshot.name %></a></li>
272
- <% }) %>
273
- </ul>
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
  <% }) %>