plum-e2e 1.0.10 → 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/.claude/settings.local.json +16 -1
- package/.vscode/settings.json +10 -0
- package/README.md +151 -37
- package/backend/_scaffold/features/LoginPage.feature +45 -3
- package/backend/_scaffold/pages/HomepagePage.ts +7 -0
- package/backend/_scaffold/pages/LoginPage.ts +37 -13
- package/backend/_scaffold/step_definitions/HomepageSteps.ts +6 -0
- package/backend/_scaffold/step_definitions/LoginSteps.ts +30 -4
- package/backend/_scaffold/utils/browser.ts +33 -0
- package/backend/_scaffold/utils/hooks.ts +8 -29
- package/backend/_scaffold/utils/utils.ts +3 -9
- package/backend/config/scripts/create-settings.js +7 -14
- package/backend/config/scripts/create-step.mjs +268 -0
- package/backend/config/scripts/generate-report.js +31 -75
- package/backend/config/scripts/run-tests.js +19 -4
- package/backend/package-lock.json +56 -641
- package/backend/package.json +4 -1
- package/backend/routes/reports.routes.js +6 -10
- package/backend/services/envService.js +4 -10
- package/backend/services/reportService.js +70 -20
- package/backend/services/testService.js +99 -24
- package/backend/tsconfig.json +2 -2
- package/backend/websockets/socketHandler.js +12 -6
- package/bin/plum.js +49 -3
- package/frontend/package-lock.json +436 -135
- package/frontend/package.json +1 -1
- package/frontend/src/app.css +241 -6
- package/frontend/src/app.html +14 -1
- package/frontend/src/lib/api/reports.js +68 -0
- package/frontend/src/lib/api/schedules.js +64 -0
- package/frontend/src/lib/api/tests.js +41 -0
- package/frontend/src/lib/components/layout/Nav.svelte +304 -0
- package/frontend/src/lib/components/layout/PageShell.svelte +28 -0
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +378 -0
- package/frontend/src/lib/components/ui/Badge.svelte +63 -0
- package/frontend/src/lib/components/ui/Button.svelte +117 -0
- package/frontend/src/lib/components/ui/Modal.svelte +140 -0
- package/frontend/src/lib/components/ui/Pagination.svelte +100 -0
- package/frontend/src/lib/components/ui/Terminal.svelte +100 -0
- package/frontend/src/lib/stores/runner.js +55 -0
- package/frontend/src/lib/stores/theme.js +47 -0
- package/frontend/src/routes/+layout.svelte +7 -12
- package/frontend/src/routes/+page.svelte +690 -142
- package/frontend/src/routes/reports/+page.svelte +395 -125
- package/frontend/src/routes/reports/[slug]/+page.svelte +749 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +267 -303
- package/frontend/svelte.config.js +1 -4
- package/frontend/tailwind.config.js +2 -23
- package/package.json +2 -2
- package/backend/_scaffold/utils/world.ts +0 -25
- package/frontend/src/routes/components/Navigation.svelte +0 -53
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is part of Plum.
|
|
3
|
+
|
|
4
|
+
Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
Plum is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
import * as clack from '@clack/prompts';
|
|
18
|
+
import pc from 'picocolors';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
const testsRoot = process.env.TESTS_ROOT || path.join(process.cwd(), 'tests');
|
|
23
|
+
const stepDefsPath = path.join(testsRoot, 'step_definitions');
|
|
24
|
+
const pagesPath = path.join(testsRoot, 'pages');
|
|
25
|
+
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* Helpers */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
|
|
30
|
+
function capitalize(str) {
|
|
31
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toMethodName(stepText) {
|
|
35
|
+
return stepText
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.map((word, i) => (i === 0 ? word : capitalize(word)))
|
|
41
|
+
.join('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toClassName(name) {
|
|
45
|
+
return capitalize(name.trim()) + 'Page';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toStepFileName(name) {
|
|
49
|
+
return capitalize(name.trim()) + 'Steps.ts';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toPageFileName(name) {
|
|
53
|
+
return toClassName(name) + '.ts';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
/* File generators */
|
|
58
|
+
/* ------------------------------------------------------------------ */
|
|
59
|
+
|
|
60
|
+
function generatePageFile(pageClassName, methodName) {
|
|
61
|
+
return `import { page } from '../utils/browser';
|
|
62
|
+
|
|
63
|
+
export class ${pageClassName} {
|
|
64
|
+
\tstatic async ${methodName}() {
|
|
65
|
+
\t\t// TODO: implement
|
|
66
|
+
\t}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateStepFile(stepType, stepText, methodName, pageClassName, pageBaseName) {
|
|
72
|
+
return `import { ${stepType} } from '@cucumber/cucumber';
|
|
73
|
+
import { ${pageClassName} } from '../pages/${pageBaseName}';
|
|
74
|
+
|
|
75
|
+
${stepType}('${stepText}', async () => {
|
|
76
|
+
\tawait ${pageClassName}.${methodName}();
|
|
77
|
+
});
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendMethodToPage(filePath, methodName) {
|
|
82
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
const method = `\n\tstatic async ${methodName}() {\n\t\t// TODO: implement\n\t}\n`;
|
|
84
|
+
const lastBrace = content.lastIndexOf('}');
|
|
85
|
+
content = content.slice(0, lastBrace) + method + content.slice(lastBrace);
|
|
86
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function appendStepToFile(filePath, stepType, stepText, methodName, pageClassName, pageBaseName) {
|
|
90
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
91
|
+
|
|
92
|
+
// Update @cucumber/cucumber import to include the new step type if missing
|
|
93
|
+
const cucumberImportMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*'@cucumber\/cucumber';/);
|
|
94
|
+
if (cucumberImportMatch) {
|
|
95
|
+
const existing = cucumberImportMatch[1].split(',').map((s) => s.trim());
|
|
96
|
+
if (!existing.includes(stepType)) {
|
|
97
|
+
const updated = [...existing, stepType].join(', ');
|
|
98
|
+
content = content.replace(
|
|
99
|
+
cucumberImportMatch[0],
|
|
100
|
+
`import { ${updated} } from '@cucumber/cucumber';`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add page import if missing
|
|
106
|
+
const pageImportLine = `import { ${pageClassName} } from '../pages/${pageBaseName}';`;
|
|
107
|
+
if (!content.includes(pageImportLine)) {
|
|
108
|
+
const lastImportIdx = content.lastIndexOf('\nimport ');
|
|
109
|
+
const insertAt = content.indexOf('\n', lastImportIdx + 1) + 1;
|
|
110
|
+
content = content.slice(0, insertAt) + pageImportLine + '\n' + content.slice(insertAt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stepBlock = `\n${stepType}('${stepText}', async () => {\n\tawait ${pageClassName}.${methodName}();\n});\n`;
|
|
114
|
+
content = content.trimEnd() + '\n' + stepBlock;
|
|
115
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ------------------------------------------------------------------ */
|
|
119
|
+
/* Main */
|
|
120
|
+
/* ------------------------------------------------------------------ */
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Create Step ')));
|
|
124
|
+
|
|
125
|
+
// 1. Step type
|
|
126
|
+
const stepTypeChoice = await clack.select({
|
|
127
|
+
message: 'Step type',
|
|
128
|
+
options: [
|
|
129
|
+
{ value: 'Given', label: 'Given', hint: 'initial context' },
|
|
130
|
+
{ value: 'When', label: 'When', hint: 'action' },
|
|
131
|
+
{ value: 'And', label: 'And', hint: 'continuation (uses When)' },
|
|
132
|
+
{ value: 'Then', label: 'Then', hint: 'expected outcome' }
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
if (clack.isCancel(stepTypeChoice)) {
|
|
136
|
+
clack.cancel('Cancelled.');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
const stepType = stepTypeChoice === 'And' ? 'When' : stepTypeChoice;
|
|
140
|
+
|
|
141
|
+
// 2. Step text
|
|
142
|
+
const stepText = await clack.text({
|
|
143
|
+
message: 'Step text',
|
|
144
|
+
placeholder: 'I am on the login page',
|
|
145
|
+
validate: (v) => (!v.trim() ? 'Step text is required' : undefined)
|
|
146
|
+
});
|
|
147
|
+
if (clack.isCancel(stepText)) {
|
|
148
|
+
clack.cancel('Cancelled.');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Step definition file
|
|
153
|
+
const existingFiles = fs.existsSync(stepDefsPath)
|
|
154
|
+
? fs.readdirSync(stepDefsPath).filter((f) => f.endsWith('.ts'))
|
|
155
|
+
: [];
|
|
156
|
+
|
|
157
|
+
const stepFile = await clack.select({
|
|
158
|
+
message: 'Add to step definition file',
|
|
159
|
+
options: [
|
|
160
|
+
{ value: '__new__', label: pc.green('+ New Step Definition') },
|
|
161
|
+
...existingFiles.map((f) => ({ value: f, label: f }))
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
if (clack.isCancel(stepFile)) {
|
|
165
|
+
clack.cancel('Cancelled.');
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let stepFileName;
|
|
170
|
+
if (stepFile === '__new__') {
|
|
171
|
+
const newName = await clack.text({
|
|
172
|
+
message: 'Step definition name',
|
|
173
|
+
placeholder: 'home',
|
|
174
|
+
hint: 'e.g. "home" → HomeSteps.ts',
|
|
175
|
+
validate: (v) => (!v.trim() ? 'Name is required' : undefined)
|
|
176
|
+
});
|
|
177
|
+
if (clack.isCancel(newName)) {
|
|
178
|
+
clack.cancel('Cancelled.');
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
stepFileName = toStepFileName(newName);
|
|
182
|
+
} else {
|
|
183
|
+
stepFileName = stepFile;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. Page selection
|
|
187
|
+
const existingPages = fs.existsSync(pagesPath)
|
|
188
|
+
? fs.readdirSync(pagesPath).filter((f) => f.endsWith('.ts'))
|
|
189
|
+
: [];
|
|
190
|
+
|
|
191
|
+
const pageChoice = await clack.select({
|
|
192
|
+
message: 'Which page does this step use?',
|
|
193
|
+
options: [
|
|
194
|
+
{ value: '__new__', label: pc.green('+ New Page') },
|
|
195
|
+
...existingPages.map((f) => ({ value: f, label: f }))
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
if (clack.isCancel(pageChoice)) {
|
|
199
|
+
clack.cancel('Cancelled.');
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let pageFileName;
|
|
204
|
+
if (pageChoice === '__new__') {
|
|
205
|
+
const newPageName = await clack.text({
|
|
206
|
+
message: 'Page name',
|
|
207
|
+
placeholder: 'home',
|
|
208
|
+
hint: '"Page.ts" will be appended — e.g. "home" → HomePage.ts',
|
|
209
|
+
validate: (v) => (!v.trim() ? 'Page name is required' : undefined)
|
|
210
|
+
});
|
|
211
|
+
if (clack.isCancel(newPageName)) {
|
|
212
|
+
clack.cancel('Cancelled.');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
pageFileName = toPageFileName(newPageName);
|
|
216
|
+
} else {
|
|
217
|
+
pageFileName = pageChoice;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pageClassName = pageFileName.replace('.ts', '');
|
|
221
|
+
const pageBaseName = pageFileName.replace('.ts', '');
|
|
222
|
+
const methodName = toMethodName(stepText);
|
|
223
|
+
|
|
224
|
+
const s = clack.spinner();
|
|
225
|
+
s.start('Generating files...');
|
|
226
|
+
|
|
227
|
+
// Handle page file
|
|
228
|
+
const pageFilePath = path.join(pagesPath, pageFileName);
|
|
229
|
+
fs.mkdirSync(pagesPath, { recursive: true });
|
|
230
|
+
if (!fs.existsSync(pageFilePath)) {
|
|
231
|
+
fs.writeFileSync(pageFilePath, generatePageFile(pageClassName, methodName), 'utf8');
|
|
232
|
+
s.stop(pc.green(`✓ Created ${pageFileName}`));
|
|
233
|
+
} else {
|
|
234
|
+
appendMethodToPage(pageFilePath, methodName);
|
|
235
|
+
s.stop(pc.cyan(`↳ Added ${methodName}() to ${pageFileName}`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle step definition file
|
|
239
|
+
const stepFilePath = path.join(stepDefsPath, stepFileName);
|
|
240
|
+
fs.mkdirSync(stepDefsPath, { recursive: true });
|
|
241
|
+
if (!fs.existsSync(stepFilePath)) {
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
stepFilePath,
|
|
244
|
+
generateStepFile(stepType, stepText, methodName, pageClassName, pageBaseName),
|
|
245
|
+
'utf8'
|
|
246
|
+
);
|
|
247
|
+
clack.log.success(pc.green(`Created ${stepFileName}`));
|
|
248
|
+
} else {
|
|
249
|
+
appendStepToFile(stepFilePath, stepType, stepText, methodName, pageClassName, pageBaseName);
|
|
250
|
+
clack.log.info(pc.cyan(`Added step to ${stepFileName}`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
clack.note(
|
|
254
|
+
[
|
|
255
|
+
`${pc.dim('Step:')} ${pc.white(`${stepType}('${stepText}')`)}`,
|
|
256
|
+
`${pc.dim('Page:')} ${pc.white(`${pageClassName}.${methodName}()`)}`,
|
|
257
|
+
`${pc.dim('Files:')} ${pc.white(stepFileName)} ${pc.dim('+')} ${pc.white(pageFileName)}`
|
|
258
|
+
].join('\n'),
|
|
259
|
+
'Summary'
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
clack.outro(pc.magenta('Done! Remember to implement the page method.'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
main().catch((err) => {
|
|
266
|
+
clack.log.error(err.message);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
});
|
|
@@ -15,50 +15,34 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const reporter = require('cucumber-html-reporter');
|
|
19
18
|
const fs = require('fs');
|
|
20
19
|
const path = require('path');
|
|
21
20
|
const { reportsHistory } = require('../settings.json');
|
|
22
21
|
|
|
23
|
-
/* -----------------------------------------------------
|
|
24
|
-
* Screenshot and Report Management
|
|
25
|
-
*
|
|
26
|
-
* Description:
|
|
27
|
-
* To avoid reports and screenshots extract
|
|
28
|
-
* status, tags, run name, and date.
|
|
29
|
-
* ------------------------------------------------------ */
|
|
30
|
-
|
|
31
22
|
const reportsDir = 'reports';
|
|
32
23
|
const screenshotsDir = path.join(reportsDir, 'screenshots');
|
|
33
24
|
const maxReports = reportsHistory;
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
if (!fs.existsSync(
|
|
37
|
-
fs.mkdirSync(reportsDir, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!fs.existsSync(screenshotsDir)) {
|
|
41
|
-
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
42
|
-
}
|
|
26
|
+
if (!fs.existsSync(reportsDir)) fs.mkdirSync(reportsDir, { recursive: true });
|
|
27
|
+
if (!fs.existsSync(screenshotsDir)) fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
43
28
|
|
|
44
|
-
//
|
|
29
|
+
// Clean up old timestamped report JSON files
|
|
45
30
|
const existingReports = fs
|
|
46
31
|
.readdirSync(reportsDir)
|
|
47
|
-
.filter((
|
|
32
|
+
.filter((f) => f.endsWith('.json') && (f.startsWith('PASS_') || f.startsWith('FAIL_')))
|
|
48
33
|
.sort(
|
|
49
34
|
(a, b) =>
|
|
50
35
|
fs.statSync(path.join(reportsDir, b)).mtime - fs.statSync(path.join(reportsDir, a)).mtime
|
|
51
36
|
);
|
|
52
37
|
|
|
53
38
|
while (existingReports.length >= maxReports) {
|
|
54
|
-
|
|
55
|
-
fs.unlinkSync(path.join(reportsDir, oldestReport));
|
|
39
|
+
fs.unlinkSync(path.join(reportsDir, existingReports.pop()));
|
|
56
40
|
}
|
|
57
41
|
|
|
58
|
-
//
|
|
42
|
+
// Clean up old screenshots
|
|
59
43
|
const existingScreenshots = fs
|
|
60
44
|
.readdirSync(screenshotsDir)
|
|
61
|
-
.filter((
|
|
45
|
+
.filter((f) => f.endsWith('.png') || f.endsWith('.jpg'))
|
|
62
46
|
.sort(
|
|
63
47
|
(a, b) =>
|
|
64
48
|
fs.statSync(path.join(screenshotsDir, b)).mtime -
|
|
@@ -66,70 +50,42 @@ const existingScreenshots = fs
|
|
|
66
50
|
);
|
|
67
51
|
|
|
68
52
|
while (existingScreenshots.length >= maxReports) {
|
|
69
|
-
|
|
70
|
-
fs.unlinkSync(path.join(screenshotsDir, oldestScreenshot));
|
|
53
|
+
fs.unlinkSync(path.join(screenshotsDir, existingScreenshots.pop()));
|
|
71
54
|
}
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
* Generate the report name
|
|
75
|
-
*
|
|
76
|
-
* Description:
|
|
77
|
-
* This is where the reports page will
|
|
78
|
-
* extract status, tags, run name, and date.
|
|
79
|
-
* ------------------------------------------------------ */
|
|
80
|
-
|
|
56
|
+
// Determine PASS/FAIL from cucumber JSON output
|
|
81
57
|
const jsonReportFile = path.join(reportsDir, 'cucumber_report.json');
|
|
82
58
|
let status = 'PASS';
|
|
83
59
|
|
|
84
60
|
if (fs.existsSync(jsonReportFile)) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Ensure the JSON is not empty
|
|
88
|
-
if (reportData) {
|
|
89
|
-
const parsedData = JSON.parse(reportData);
|
|
90
|
-
|
|
61
|
+
try {
|
|
62
|
+
const parsedData = JSON.parse(fs.readFileSync(jsonReportFile, 'utf8'));
|
|
91
63
|
const hasFailures = parsedData.some((feature) =>
|
|
92
|
-
feature.elements
|
|
93
|
-
scenario.steps
|
|
64
|
+
feature.elements?.some((scenario) =>
|
|
65
|
+
scenario.steps?.some((step) => step.result?.status === 'failed')
|
|
94
66
|
)
|
|
95
67
|
);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
console.log('Report file is empty or malformed');
|
|
68
|
+
if (hasFailures) status = 'FAIL';
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('Could not parse cucumber_report.json:', e.message);
|
|
102
71
|
}
|
|
72
|
+
} else {
|
|
73
|
+
console.log('No cucumber_report.json found.');
|
|
103
74
|
}
|
|
104
75
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!tag) {
|
|
108
|
-
tag = '(@all-tests)';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (tag && !tag.startsWith('(') && !tag.endsWith(')')) {
|
|
112
|
-
tag = `(${tag})`;
|
|
113
|
-
}
|
|
76
|
+
// Build filename with same convention as before, now .json
|
|
77
|
+
let tag = process.env.TAG || '(@all-tests)';
|
|
78
|
+
if (tag && !tag.startsWith('(')) tag = `(${tag})`;
|
|
114
79
|
|
|
115
80
|
const timestamp = new Date().toISOString().replace(/[-:.]/g, '_');
|
|
116
|
-
const
|
|
81
|
+
const runners = Number.parseInt(process.env.REPORT_RUNNERS || process.env.PARALLEL || '1', 10);
|
|
82
|
+
const runnerCount = Number.isFinite(runners) && runners > 0 ? runners : 1;
|
|
83
|
+
const reportFileName = `${status}_cucumber_report_${process.env.TRIGGER}_${tag}_runners_${runnerCount}_${timestamp}.json`;
|
|
117
84
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'App Name': 'Plum',
|
|
126
|
-
'Test Environment': 'Local',
|
|
127
|
-
Browser: 'Chromium',
|
|
128
|
-
Platform: 'Ubuntu',
|
|
129
|
-
Parallel: 'Scenarios',
|
|
130
|
-
Executed: 'Local'
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
reporter.generate(options);
|
|
135
|
-
console.log(`Generated report: ${reportFileName}`);
|
|
85
|
+
// Save a timestamped snapshot of the cucumber JSON
|
|
86
|
+
if (fs.existsSync(jsonReportFile)) {
|
|
87
|
+
fs.copyFileSync(jsonReportFile, path.join(reportsDir, reportFileName));
|
|
88
|
+
console.log(`Report saved: ${reportFileName}`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log('Skipping report save — no cucumber_report.json.');
|
|
91
|
+
}
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
|
-
const
|
|
19
|
+
const pc = require('picocolors');
|
|
20
|
+
|
|
21
|
+
const parallelIdx = process.argv.indexOf('--parallel');
|
|
22
|
+
const parallel =
|
|
23
|
+
process.env.PARALLEL || (parallelIdx !== -1 ? process.argv[parallelIdx + 1] : null);
|
|
24
|
+
const runners = parallel || process.env.REPORT_RUNNERS || '1';
|
|
25
|
+
const tag = process.env.TAG || process.argv.slice(2).find((a) => a.startsWith('@'));
|
|
20
26
|
|
|
21
27
|
try {
|
|
22
28
|
const baseCommand = [
|
|
@@ -28,6 +34,8 @@ try {
|
|
|
28
34
|
'--require-module',
|
|
29
35
|
'ts-node/register',
|
|
30
36
|
'--require',
|
|
37
|
+
'tests/utils/hooks.ts',
|
|
38
|
+
'--require',
|
|
31
39
|
'tests/step_definitions/**/*.ts',
|
|
32
40
|
'--format',
|
|
33
41
|
'json:reports/cucumber_report.json'
|
|
@@ -37,14 +45,21 @@ try {
|
|
|
37
45
|
baseCommand.push('--tags', `"${tag}"`);
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
if (parallel) {
|
|
49
|
+
baseCommand.push('--parallel', parallel);
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
const cucumberCommand = baseCommand.join(' ');
|
|
41
53
|
execSync(cucumberCommand, { stdio: 'inherit' });
|
|
42
54
|
} catch (error) {
|
|
43
|
-
console.error('Tests failed:'
|
|
55
|
+
console.error(pc.red('✗') + ' Tests failed: ' + error.message);
|
|
44
56
|
} finally {
|
|
45
57
|
try {
|
|
46
|
-
execSync('node config/scripts/generate-report.js', {
|
|
58
|
+
execSync('node config/scripts/generate-report.js', {
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
env: { ...process.env, REPORT_RUNNERS: runners }
|
|
61
|
+
});
|
|
47
62
|
} catch (error) {
|
|
48
|
-
console.error('
|
|
63
|
+
console.error(pc.red('✗') + ' Report generation failed: ' + error.message);
|
|
49
64
|
}
|
|
50
65
|
}
|