plum-e2e 2.4.3 → 2.4.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/backend/_scaffold/features/LoginPage.feature +5 -5
- package/backend/routes/node.routes.js +3 -1
- package/backend/server.js +8 -0
- package/backend/services/reportService.js +21 -0
- package/backend/services/testCaseService.js +18 -1
- package/frontend/src/routes/settings/+page.svelte +27 -10
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@
|
|
1
|
+
@TS-001
|
|
2
2
|
Feature: Demo Sauce Login
|
|
3
3
|
|
|
4
4
|
# Background runs before every scenario in this file
|
|
@@ -6,14 +6,14 @@ Feature: Demo Sauce Login
|
|
|
6
6
|
Given I am in Demo Sauce Login page
|
|
7
7
|
|
|
8
8
|
# ── Basic Scenario ─────────────────────────────────────────────────────────
|
|
9
|
-
@
|
|
9
|
+
@TC-001
|
|
10
10
|
Scenario: User can log in with valid credentials
|
|
11
11
|
When I enter "standard_user" in username field
|
|
12
12
|
And I enter "secret_sauce" in password field
|
|
13
13
|
And I click on the login button
|
|
14
14
|
Then I should be navigated to the products page
|
|
15
15
|
|
|
16
|
-
@
|
|
16
|
+
@TC-002
|
|
17
17
|
Scenario: User cannot log in with invalid credentials
|
|
18
18
|
When I enter "invalid_user" in username field
|
|
19
19
|
And I enter "invalid_password" in password field
|
|
@@ -23,7 +23,7 @@ Feature: Demo Sauce Login
|
|
|
23
23
|
# ── Scenario Outline ───────────────────────────────────────────────────────
|
|
24
24
|
# Runs once per row in the Examples table.
|
|
25
25
|
# Use <placeholder> in steps to reference column headers.
|
|
26
|
-
@
|
|
26
|
+
@TC-003
|
|
27
27
|
Scenario Outline: User login attempts with different credentials
|
|
28
28
|
When I enter "<username>" in username field
|
|
29
29
|
And I enter "<password>" in password field
|
|
@@ -39,7 +39,7 @@ Feature: Demo Sauce Login
|
|
|
39
39
|
# ── Data Table ─────────────────────────────────────────────────────────────
|
|
40
40
|
# Pass structured data directly into a step.
|
|
41
41
|
# Access rows in your step definition via dataTable.hashes().
|
|
42
|
-
@
|
|
42
|
+
@TC-004
|
|
43
43
|
Scenario: User can log in using a data table
|
|
44
44
|
When I fill in the login form:
|
|
45
45
|
| field | value |
|
|
@@ -25,6 +25,8 @@ const path = require('path');
|
|
|
25
25
|
const { authGuard } = require('../middleware/auth');
|
|
26
26
|
const { TRIGGER_REMOTE } = require('../constants/triggers');
|
|
27
27
|
|
|
28
|
+
const BACKEND_DIR = path.resolve(__dirname, '..');
|
|
29
|
+
|
|
28
30
|
// In-memory job store for active remote executions.
|
|
29
31
|
// Jobs are purged after 10 minutes post-completion.
|
|
30
32
|
const jobs = {};
|
|
@@ -84,7 +86,7 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
84
86
|
};
|
|
85
87
|
if (workers > 1) env.PARALLEL = String(workers);
|
|
86
88
|
|
|
87
|
-
const proc = spawn('npm', ['run', 'test'], { env, shell: true });
|
|
89
|
+
const proc = spawn('npm', ['run', 'test'], { env, shell: true, cwd: BACKEND_DIR });
|
|
88
90
|
proc.stdout.on('data', (d) => {
|
|
89
91
|
jobs[jobId].logs += d.toString();
|
|
90
92
|
});
|
package/backend/server.js
CHANGED
|
@@ -87,6 +87,11 @@ async function start() {
|
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Sync automated flags from feature files on every startup
|
|
91
|
+
require('./services/reportService')
|
|
92
|
+
.syncAutomatedFromFeatures()
|
|
93
|
+
.catch(() => {});
|
|
94
|
+
|
|
90
95
|
// chokidar v5+ is ESM-only — use dynamic import to stay compatible with CJS
|
|
91
96
|
let chokidar;
|
|
92
97
|
try {
|
|
@@ -109,6 +114,9 @@ async function start() {
|
|
|
109
114
|
`📝 Tests changed (${event}: ${path.basename(filePath)}) — notifying clients`
|
|
110
115
|
);
|
|
111
116
|
io.emit('tests-changed');
|
|
117
|
+
require('./services/reportService')
|
|
118
|
+
.syncAutomatedFromFeatures()
|
|
119
|
+
.catch(() => {});
|
|
112
120
|
}, 300);
|
|
113
121
|
});
|
|
114
122
|
console.log('👀 Watching for test file changes...');
|
|
@@ -400,12 +400,33 @@ const deleteReports = async (ids) => {
|
|
|
400
400
|
await prisma.report.deleteMany({ where: { id: { in: ids } } });
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
const FEATURES_DIR = path.join(__dirname, '../tests/features');
|
|
404
|
+
|
|
405
|
+
async function syncAutomatedFromFeatures() {
|
|
406
|
+
try {
|
|
407
|
+
if (!fs.existsSync(FEATURES_DIR)) return;
|
|
408
|
+
const tagSet = new Set();
|
|
409
|
+
for (const file of fs.readdirSync(FEATURES_DIR).filter((f) => f.endsWith('.feature'))) {
|
|
410
|
+
const content = fs.readFileSync(path.join(FEATURES_DIR, file), 'utf8');
|
|
411
|
+
for (const m of content.matchAll(/@(\S+)/g)) tagSet.add(m[1]);
|
|
412
|
+
}
|
|
413
|
+
if (tagSet.size === 0) return;
|
|
414
|
+
await prisma.testCase.updateMany({
|
|
415
|
+
where: { displayId: { in: [...tagSet] }, isAutomated: false },
|
|
416
|
+
data: { isAutomated: true }
|
|
417
|
+
});
|
|
418
|
+
} catch (e) {
|
|
419
|
+
console.error('[sync] syncAutomatedFromFeatures failed:', e.message);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
403
423
|
module.exports = {
|
|
404
424
|
getAllReports,
|
|
405
425
|
getLatestReportId,
|
|
406
426
|
getReportDetail,
|
|
407
427
|
saveReport,
|
|
408
428
|
saveCombinedReport,
|
|
429
|
+
syncAutomatedFromFeatures,
|
|
409
430
|
deleteReport,
|
|
410
431
|
deleteReports
|
|
411
432
|
};
|
|
@@ -15,8 +15,24 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
18
20
|
const prisma = require('./prisma');
|
|
19
21
|
|
|
22
|
+
const FEATURES_DIR = path.join(__dirname, '../tests/features');
|
|
23
|
+
|
|
24
|
+
function isTaggedInFeatures(displayId) {
|
|
25
|
+
try {
|
|
26
|
+
const tag = `@${displayId}`;
|
|
27
|
+
return fs
|
|
28
|
+
.readdirSync(FEATURES_DIR)
|
|
29
|
+
.filter((f) => f.endsWith('.feature'))
|
|
30
|
+
.some((f) => fs.readFileSync(path.join(FEATURES_DIR, f), 'utf8').includes(tag));
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
const caseSelect = {
|
|
21
37
|
id: true,
|
|
22
38
|
displayId: true,
|
|
@@ -75,7 +91,8 @@ async function create({ suiteId, title, description, priority, createdById }) {
|
|
|
75
91
|
title,
|
|
76
92
|
description: description ?? '',
|
|
77
93
|
priority: priority ?? 'Medium',
|
|
78
|
-
createdById
|
|
94
|
+
createdById,
|
|
95
|
+
isAutomated: isTaggedInFeatures(displayId)
|
|
79
96
|
},
|
|
80
97
|
select: caseSelect
|
|
81
98
|
});
|
|
@@ -578,16 +578,33 @@
|
|
|
578
578
|
2
|
|
579
579
|
);
|
|
580
580
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
581
|
+
const ADMIN_SECTIONS = new Set([
|
|
582
|
+
'project',
|
|
583
|
+
'runners',
|
|
584
|
+
'repository',
|
|
585
|
+
'integrations',
|
|
586
|
+
'mcp',
|
|
587
|
+
'users',
|
|
588
|
+
'backup'
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
$: if ($auth.user && $auth.user.role !== 'admin' && ADMIN_SECTIONS.has(section)) {
|
|
592
|
+
section = 'account';
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
$: navItems =
|
|
596
|
+
$auth.user?.role === 'admin'
|
|
597
|
+
? [
|
|
598
|
+
{ id: 'project', label: 'Project' },
|
|
599
|
+
{ id: 'runners', label: 'Runners' },
|
|
600
|
+
{ id: 'repository', label: 'Repository' },
|
|
601
|
+
{ id: 'integrations', label: 'Integrations' },
|
|
602
|
+
{ id: 'mcp', label: 'MCP' },
|
|
603
|
+
{ id: 'account', label: 'Account' },
|
|
604
|
+
{ id: 'users', label: 'Users' },
|
|
605
|
+
{ id: 'backup', label: 'Backup' }
|
|
606
|
+
]
|
|
607
|
+
: [{ id: 'account', label: 'Account' }];
|
|
591
608
|
</script>
|
|
592
609
|
|
|
593
610
|
<svelte:head><title>Settings — Plum</title></svelte:head>
|