plum-e2e 1.0.9 → 1.1.0
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
package/backend/package.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
|
+
"init": "node services/envService.js && node config/scripts/create-settings.js",
|
|
7
|
+
"create-step": "node config/scripts/create-step.mjs",
|
|
6
8
|
"create-env": "node services/envService.js",
|
|
7
9
|
"test": "node config/scripts/run-tests.js"
|
|
8
10
|
},
|
|
@@ -13,11 +15,11 @@
|
|
|
13
15
|
"@playwright/test": "^1.50.1",
|
|
14
16
|
"@types/node": "^22.17.0",
|
|
15
17
|
"cross-env": "^7.0.3",
|
|
16
|
-
"cucumber-html-reporter": "^7.2.0",
|
|
17
18
|
"ts-node": "^10.9.2",
|
|
18
19
|
"typescript": "^5.9.2"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
22
|
+
"@clack/prompts": "^1.5.1",
|
|
21
23
|
"@cucumber/cucumber": "^11.2.0",
|
|
22
24
|
"chai": "^4.3.6",
|
|
23
25
|
"chai-soft-assert": "^0.0.5",
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
"dotenv": "^16.4.7",
|
|
26
28
|
"express": "^4.21.2",
|
|
27
29
|
"node-cron": "^3.0.3",
|
|
30
|
+
"picocolors": "^1.1.1",
|
|
28
31
|
"playwright": "^1.50.1",
|
|
29
32
|
"socket.io": "^4.8.1"
|
|
30
33
|
}
|
|
@@ -19,24 +19,20 @@ const express = require('express');
|
|
|
19
19
|
const router = express.Router();
|
|
20
20
|
const reportService = require('../services/reportService');
|
|
21
21
|
|
|
22
|
-
/* -----------------------------------------------------
|
|
23
|
-
* Get Reports
|
|
24
|
-
* Description:
|
|
25
|
-
* Get all reports from reports/
|
|
26
|
-
* ------------------------------------------------------ */
|
|
27
22
|
router.get('/', (req, res) => {
|
|
28
23
|
const reports = reportService.getAllReports();
|
|
29
24
|
res.json({ reports });
|
|
30
25
|
});
|
|
31
26
|
|
|
32
|
-
/* -----------------------------------------------------
|
|
33
|
-
* Get Latest Report
|
|
34
|
-
* Description:
|
|
35
|
-
* Get latest report
|
|
36
|
-
* ------------------------------------------------------ */
|
|
37
27
|
router.get('/latest', (req, res) => {
|
|
38
28
|
const latestReport = reportService.getLatestReport();
|
|
39
29
|
res.json({ latestReport });
|
|
40
30
|
});
|
|
41
31
|
|
|
32
|
+
router.get('/:fileName/detail', (req, res) => {
|
|
33
|
+
const detail = reportService.getReportDetail(req.params.fileName);
|
|
34
|
+
if (!detail) return res.status(404).json({ error: 'Report not found' });
|
|
35
|
+
res.json(detail);
|
|
36
|
+
});
|
|
37
|
+
|
|
42
38
|
module.exports = router;
|
|
@@ -17,27 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const pc = require('picocolors');
|
|
20
21
|
|
|
21
|
-
/* -----------------------------------------------------
|
|
22
|
-
* .env Generator
|
|
23
|
-
* Description:
|
|
24
|
-
* If .env file doesn't exist, this will create
|
|
25
|
-
* a .env file in root with sauce demo's base URL
|
|
26
|
-
* ------------------------------------------------------ */
|
|
27
22
|
const envPath = path.join(__dirname, '../.env');
|
|
28
23
|
|
|
29
24
|
if (!fs.existsSync(envPath)) {
|
|
30
25
|
const envContent = 'BASE_URL=https://www.saucedemo.com/v1/\nIS_HEADLESS=false';
|
|
31
26
|
fs.writeFileSync(envPath, envContent);
|
|
32
|
-
console.log('.env
|
|
27
|
+
console.log(pc.green('✓') + ' .env created with default values.');
|
|
33
28
|
} else {
|
|
34
29
|
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
35
|
-
|
|
36
30
|
if (!envContent.includes('IS_HEADLESS=')) {
|
|
37
31
|
envContent += '\nIS_HEADLESS=false';
|
|
38
32
|
fs.writeFileSync(envPath, envContent);
|
|
39
|
-
console.log('IS_HEADLESS=false added to .env');
|
|
33
|
+
console.log(pc.cyan('↳') + ' IS_HEADLESS=false added to .env.');
|
|
40
34
|
} else {
|
|
41
|
-
console.log('.env
|
|
35
|
+
console.log(pc.yellow('⚠') + ' .env already exists. Skipping.');
|
|
42
36
|
}
|
|
43
37
|
}
|
|
@@ -22,29 +22,79 @@ const REPORTS_DIR = path.join(__dirname, '../reports');
|
|
|
22
22
|
|
|
23
23
|
const getAllReports = () => {
|
|
24
24
|
const files = fs.readdirSync(REPORTS_DIR);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const bStats = fs.statSync(path.join(REPORTS_DIR, b));
|
|
33
|
-
return bStats.mtime - aStats.mtime; // Sort in descending order of modification time
|
|
25
|
+
const jsonFiles = files.filter(
|
|
26
|
+
(f) => f.endsWith('.json') && (f.startsWith('PASS_') || f.startsWith('FAIL_'))
|
|
27
|
+
);
|
|
28
|
+
return jsonFiles.sort((a, b) => {
|
|
29
|
+
const at = fs.statSync(path.join(REPORTS_DIR, a)).mtime;
|
|
30
|
+
const bt = fs.statSync(path.join(REPORTS_DIR, b)).mtime;
|
|
31
|
+
return bt - at;
|
|
34
32
|
});
|
|
35
|
-
|
|
36
|
-
return sortedFiles;
|
|
37
33
|
};
|
|
38
34
|
|
|
39
35
|
const getLatestReport = () => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
const files = getAllReports();
|
|
37
|
+
return files.length ? files[0] : null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getReportDetail = (fileName) => {
|
|
41
|
+
const filePath = path.join(REPORTS_DIR, fileName);
|
|
42
|
+
if (!filePath.startsWith(REPORTS_DIR)) return null; // path traversal guard
|
|
43
|
+
if (!fs.existsSync(filePath)) return null;
|
|
44
|
+
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const features = raw.map((feature) => {
|
|
53
|
+
const scenarios = (feature.elements || []).map((scenario) => {
|
|
54
|
+
const visibleSteps = (scenario.steps || []).filter((s) => !s.hidden);
|
|
55
|
+
const hookScreenshots = (scenario.steps || [])
|
|
56
|
+
.filter((s) => s.hidden)
|
|
57
|
+
.flatMap((step) => step.embeddings?.filter((e) => e.mime_type === 'image/png') ?? []);
|
|
58
|
+
const failedStepIndex = visibleSteps.findLastIndex((s) => s.result?.status === 'failed');
|
|
59
|
+
|
|
60
|
+
const steps = visibleSteps.map((step, index) => ({
|
|
61
|
+
keyword: step.keyword.trim(),
|
|
62
|
+
name: step.name ?? '',
|
|
63
|
+
status: step.result?.status ?? 'pending',
|
|
64
|
+
duration: Math.round((step.result?.duration ?? 0) / 1_000_000), // ns → ms
|
|
65
|
+
error: step.result?.error_message ?? null,
|
|
66
|
+
screenshot:
|
|
67
|
+
step.embeddings?.find((e) => e.mime_type === 'image/png')?.data ??
|
|
68
|
+
(index === failedStepIndex ? hookScreenshots[0]?.data : null) ??
|
|
69
|
+
null
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const worstStatus = steps.reduce((acc, s) => {
|
|
73
|
+
const rank = { failed: 3, pending: 2, skipped: 1, passed: 0 };
|
|
74
|
+
return (rank[s.status] ?? 0) > (rank[acc] ?? 0) ? s.status : acc;
|
|
75
|
+
}, 'passed');
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name: scenario.name,
|
|
79
|
+
keyword: scenario.keyword,
|
|
80
|
+
tags: (scenario.tags ?? []).map((t) => t.name),
|
|
81
|
+
status: worstStatus,
|
|
82
|
+
duration: steps.reduce((s, st) => s + st.duration, 0),
|
|
83
|
+
steps
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const featureStatus = scenarios.some((s) => s.status === 'failed') ? 'failed' : 'passed';
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
name: feature.name,
|
|
91
|
+
uri: feature.uri,
|
|
92
|
+
status: featureStatus,
|
|
93
|
+
scenarios
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { features };
|
|
48
98
|
};
|
|
49
99
|
|
|
50
|
-
module.exports = { getAllReports, getLatestReport };
|
|
100
|
+
module.exports = { getAllReports, getLatestReport, getReportDetail };
|
|
@@ -22,45 +22,120 @@ const FEATURES_DIR = path.join(__dirname, '../tests/features');
|
|
|
22
22
|
|
|
23
23
|
const getTestSuites = () => {
|
|
24
24
|
const suites = [];
|
|
25
|
-
|
|
26
|
-
const files = fs.readdirSync(FEATURES_DIR).filter((file) => file.endsWith('.feature'));
|
|
25
|
+
const files = fs.readdirSync(FEATURES_DIR).filter((f) => f.endsWith('.feature'));
|
|
27
26
|
|
|
28
27
|
files.forEach((file) => {
|
|
29
28
|
const content = fs.readFileSync(path.join(FEATURES_DIR, file), 'utf8');
|
|
29
|
+
const lines = content.split('\n');
|
|
30
30
|
|
|
31
31
|
let suiteName = '';
|
|
32
32
|
let suiteTags = [];
|
|
33
|
+
let backgroundSteps = [];
|
|
33
34
|
const tests = [];
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
36
|
+
let inBackground = false;
|
|
37
|
+
let inExamples = false;
|
|
38
|
+
let pendingTags = [];
|
|
39
|
+
let currentTest = null;
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const finalizeTest = () => {
|
|
42
|
+
if (currentTest) {
|
|
43
|
+
tests.push(currentTest);
|
|
44
|
+
currentTest = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const tags = match[1].split(/\s+/).filter((tag) => tag.trim() !== ''); // Remove empty elements
|
|
50
|
-
const scenarioText = match[2].trim();
|
|
48
|
+
for (const raw of lines) {
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Tag line
|
|
53
|
+
if (trimmed.startsWith('@')) {
|
|
54
|
+
finalizeTest();
|
|
55
|
+
inBackground = false;
|
|
56
|
+
inExamples = false;
|
|
57
|
+
const tags = trimmed.split(/\s+/).filter(Boolean);
|
|
58
|
+
if (!suiteName) {
|
|
59
|
+
suiteTags = [...suiteTags, ...tags];
|
|
60
|
+
} else {
|
|
61
|
+
pendingTags = [...pendingTags, ...tags];
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
// Feature
|
|
67
|
+
if (trimmed.startsWith('Feature:')) {
|
|
68
|
+
suiteName = trimmed.replace('Feature:', '').trim();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
// Background
|
|
73
|
+
if (trimmed.startsWith('Background:')) {
|
|
74
|
+
inBackground = true;
|
|
75
|
+
inExamples = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Scenario or Scenario Outline
|
|
80
|
+
const scenarioMatch = trimmed.match(/^Scenario(?:\s+Outline)?:\s*(.+)/i);
|
|
81
|
+
if (scenarioMatch) {
|
|
82
|
+
finalizeTest();
|
|
83
|
+
inBackground = false;
|
|
84
|
+
inExamples = false;
|
|
85
|
+
|
|
86
|
+
const isOutline = /^Scenario\s+Outline:/i.test(trimmed);
|
|
87
|
+
const tags = pendingTags.splice(0);
|
|
88
|
+
const [testId, ...extraTags] = tags;
|
|
89
|
+
|
|
90
|
+
currentTest = {
|
|
91
|
+
id: extraTags.length > 0 ? [testId, ...extraTags] : testId,
|
|
92
|
+
testCase: scenarioMatch[1].trim(),
|
|
93
|
+
type: isOutline ? 'outline' : 'scenario',
|
|
94
|
+
steps: [...backgroundSteps],
|
|
95
|
+
...(isOutline ? { examples: null } : {})
|
|
96
|
+
};
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Examples header (Scenario Outline)
|
|
101
|
+
if (trimmed.startsWith('Examples:')) {
|
|
102
|
+
inExamples = true;
|
|
103
|
+
if (currentTest) currentTest.examples = { headers: [], rows: [] };
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step lines
|
|
108
|
+
if (/^(Given|When|Then|And|But)\s/i.test(trimmed)) {
|
|
109
|
+
if (inBackground) {
|
|
110
|
+
backgroundSteps.push(trimmed);
|
|
111
|
+
} else if (currentTest && !inExamples) {
|
|
112
|
+
currentTest.steps.push(trimmed);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Examples table rows
|
|
118
|
+
if (inExamples && trimmed.startsWith('|') && currentTest?.examples) {
|
|
119
|
+
const cells = trimmed
|
|
120
|
+
.split('|')
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((c) => c.trim());
|
|
123
|
+
if (currentTest.examples.headers.length === 0) {
|
|
124
|
+
currentTest.examples.headers = cells;
|
|
125
|
+
} else {
|
|
126
|
+
currentTest.examples.rows.push(cells);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
finalizeTest();
|
|
61
132
|
|
|
62
133
|
if (suiteName && tests.length) {
|
|
63
|
-
suites.push({
|
|
134
|
+
suites.push({
|
|
135
|
+
suiteName,
|
|
136
|
+
suiteId: suiteTags.length > 1 ? suiteTags : suiteTags[0],
|
|
137
|
+
tests
|
|
138
|
+
});
|
|
64
139
|
}
|
|
65
140
|
});
|
|
66
141
|
|
package/backend/tsconfig.json
CHANGED
|
@@ -20,12 +20,18 @@ const { spawn } = require('child_process');
|
|
|
20
20
|
const socketHandler = (io) => {
|
|
21
21
|
io.on('connection', (socket) => {
|
|
22
22
|
console.log('WebSocket connection established');
|
|
23
|
-
socket.on('run-test', (testID) => {
|
|
23
|
+
socket.on('run-test', (testID, workers) => {
|
|
24
24
|
const tag = testID ? `${testID}` : '';
|
|
25
|
+
const runnerCount = Number(workers) > 1 ? Number(workers) : 1;
|
|
26
|
+
const env = {
|
|
27
|
+
...process.env,
|
|
28
|
+
TAG: tag,
|
|
29
|
+
TRIGGER: 'manual-trigger',
|
|
30
|
+
REPORT_RUNNERS: String(runnerCount)
|
|
31
|
+
};
|
|
32
|
+
if (workers && workers > 1) env.PARALLEL = String(workers);
|
|
25
33
|
|
|
26
|
-
const testProcess = spawn('npm', ['run', 'test'], {
|
|
27
|
-
env: { ...process.env, TAG: tag, TRIGGER: 'manual-trigger' }
|
|
28
|
-
});
|
|
34
|
+
const testProcess = spawn('npm', ['run', 'test'], { env });
|
|
29
35
|
|
|
30
36
|
testProcess.stdout.on('data', (data) => {
|
|
31
37
|
socket.emit('log', data.toString());
|
|
@@ -36,8 +42,8 @@ const socketHandler = (io) => {
|
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
testProcess.on('close', (code) => {
|
|
39
|
-
socket.emit('log',
|
|
40
|
-
socket.emit('done');
|
|
45
|
+
socket.emit('log', `\nTest finished with code ${code}`);
|
|
46
|
+
socket.emit('done', code);
|
|
41
47
|
});
|
|
42
48
|
});
|
|
43
49
|
});
|
package/bin/plum.js
CHANGED
|
@@ -91,6 +91,34 @@ switch (command) {
|
|
|
91
91
|
// Create .env file with default values
|
|
92
92
|
createEnvFile();
|
|
93
93
|
|
|
94
|
+
// Create .vscode/settings.json for Cucumber extension step linkage
|
|
95
|
+
const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
|
|
96
|
+
if (!fs.existsSync(vscodeSettingsPath)) {
|
|
97
|
+
fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
|
|
98
|
+
const vscodeSettings = JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
'cucumber.glue': ['tests/step_definitions/**/*.ts'],
|
|
101
|
+
'cucumber.features': ['tests/features/**/*.feature']
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2
|
|
105
|
+
);
|
|
106
|
+
fs.writeFileSync(vscodeSettingsPath, vscodeSettings, 'utf8');
|
|
107
|
+
console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
|
|
108
|
+
} else {
|
|
109
|
+
console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Install Cucumber VS Code extension
|
|
113
|
+
try {
|
|
114
|
+
execSync('code --install-extension cucumberopen.cucumber-official', { stdio: 'inherit' });
|
|
115
|
+
console.log('✅ Cucumber VS Code extension installed.\n');
|
|
116
|
+
} catch {
|
|
117
|
+
console.log(
|
|
118
|
+
'⚠️ Could not install VS Code extension automatically. Install manually: cucumberopen.cucumber-official\n'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
// Initialize project
|
|
95
123
|
console.log('--------------------------------------\n');
|
|
96
124
|
console.log('🚀 Initializing Plum...');
|
|
@@ -152,7 +180,10 @@ switch (command) {
|
|
|
152
180
|
// Copy .env file from root to backend
|
|
153
181
|
copyEnvFile();
|
|
154
182
|
|
|
155
|
-
const
|
|
183
|
+
const devArgs = process.argv.slice(3);
|
|
184
|
+
const parallelIdx = devArgs.indexOf('--parallel');
|
|
185
|
+
const parallelArg = parallelIdx !== -1 ? devArgs[parallelIdx + 1] : null;
|
|
186
|
+
const tagArg = devArgs.find((a) => a.startsWith('@')) ?? null;
|
|
156
187
|
const userTestsPath = path.resolve(process.cwd(), 'tests');
|
|
157
188
|
const backendTestsPath = path.join(plumRoot, 'backend', 'tests');
|
|
158
189
|
|
|
@@ -184,6 +215,7 @@ switch (command) {
|
|
|
184
215
|
console.log('--------------------------------------\n');
|
|
185
216
|
console.log('Running `npm run test` with:');
|
|
186
217
|
console.log('TAG =', tagArg ?? '');
|
|
218
|
+
console.log('PARALLEL =', parallelArg ?? 'off');
|
|
187
219
|
console.log('TRIGGER =', 'command-line-trigger');
|
|
188
220
|
|
|
189
221
|
execSync('npm run test', {
|
|
@@ -192,15 +224,29 @@ switch (command) {
|
|
|
192
224
|
env: {
|
|
193
225
|
...process.env,
|
|
194
226
|
TAG: tagArg ?? '',
|
|
195
|
-
TRIGGER: 'command-line-trigger'
|
|
227
|
+
TRIGGER: 'command-line-trigger',
|
|
228
|
+
...(parallelArg ? { PARALLEL: parallelArg } : {})
|
|
196
229
|
}
|
|
197
230
|
});
|
|
198
231
|
console.log('--------------------------------------\n');
|
|
199
232
|
break;
|
|
200
233
|
}
|
|
201
234
|
|
|
235
|
+
case 'create-step': {
|
|
236
|
+
const createStepScript = path.join(plumRoot, 'backend', 'config', 'scripts', 'create-step.mjs');
|
|
237
|
+
execSync(`node ${createStepScript}`, {
|
|
238
|
+
cwd: process.cwd(),
|
|
239
|
+
stdio: 'inherit',
|
|
240
|
+
env: {
|
|
241
|
+
...process.env,
|
|
242
|
+
TESTS_ROOT: userTestsPath
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
202
248
|
default:
|
|
203
249
|
console.log('--------------------------------------\n');
|
|
204
|
-
console.log('Usage: plum <init|start|dev>');
|
|
250
|
+
console.log('Usage: plum <init|start|dev|create-step>');
|
|
205
251
|
console.log('--------------------------------------\n');
|
|
206
252
|
}
|