plum-e2e 1.3.7 → 2.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.
Files changed (45) hide show
  1. package/README.md +61 -3
  2. package/backend/app.js +5 -0
  3. package/backend/config/scripts/create-test.mjs +172 -0
  4. package/backend/config/scripts/generate-report.js +2 -1
  5. package/backend/middleware/jwtAuth.js +33 -0
  6. package/backend/middleware/requireAdmin.js +25 -0
  7. package/backend/package.json +2 -0
  8. package/backend/prisma/migrations/20260618000000_add_test_repository/migration.sql +133 -0
  9. package/backend/prisma/migrations/20260618000001_add_user_roles/migration.sql +3 -0
  10. package/backend/prisma/migrations/20260618000002_drop_automated_tag/migration.sql +2 -0
  11. package/backend/prisma/migrations/20260618000003_entry_assignee/migration.sql +2 -0
  12. package/backend/prisma/schema.prisma +118 -10
  13. package/backend/routes/auth.routes.js +96 -0
  14. package/backend/routes/settings.routes.js +44 -8
  15. package/backend/routes/test-cases.routes.js +80 -0
  16. package/backend/routes/test-runs.routes.js +122 -0
  17. package/backend/routes/test-suites.routes.js +92 -0
  18. package/backend/routes/users.routes.js +67 -0
  19. package/backend/scripts/create-test.js +7 -6
  20. package/backend/services/reportService.js +96 -4
  21. package/backend/services/settingsService.js +18 -2
  22. package/backend/services/testCaseService.js +139 -0
  23. package/backend/services/testRunService.js +203 -0
  24. package/backend/services/testSuiteService.js +191 -0
  25. package/backend/services/userService.js +114 -0
  26. package/backend/websockets/socketHandler.js +19 -6
  27. package/bin/plum.js +105 -9
  28. package/frontend/src/lib/api/auth.js +69 -0
  29. package/frontend/src/lib/api/repository.js +256 -0
  30. package/frontend/src/lib/api/users.js +52 -0
  31. package/frontend/src/lib/components/layout/Nav.svelte +116 -4
  32. package/frontend/src/lib/components/layout/RunnerPanel.svelte +243 -29
  33. package/frontend/src/lib/components/ui/Modal.svelte +8 -1
  34. package/frontend/src/lib/constants.js +2 -0
  35. package/frontend/src/lib/stores/auth.js +60 -0
  36. package/frontend/src/lib/stores/runner.js +9 -2
  37. package/frontend/src/routes/+layout.svelte +32 -4
  38. package/frontend/src/routes/+page.svelte +1 -1
  39. package/frontend/src/routes/login/+page.svelte +209 -0
  40. package/frontend/src/routes/settings/+page.svelte +586 -5
  41. package/frontend/src/routes/setup/+page.svelte +249 -0
  42. package/frontend/src/routes/test-repository/+page.svelte +1379 -0
  43. package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +1549 -0
  44. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +1490 -0
  45. package/package.json +1 -1
@@ -0,0 +1,96 @@
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
+
18
+ const express = require('express');
19
+ const router = express.Router();
20
+ const userService = require('../services/userService');
21
+ const { jwtAuth } = require('../middleware/jwtAuth');
22
+
23
+ router.get('/needs-setup', async (req, res, next) => {
24
+ try {
25
+ const setup = await userService.needsSetup();
26
+ res.json({ needsSetup: setup });
27
+ } catch (e) {
28
+ next(e);
29
+ }
30
+ });
31
+
32
+ router.post('/setup', async (req, res, next) => {
33
+ try {
34
+ const setup = await userService.needsSetup();
35
+ if (!setup) return res.status(403).json({ error: 'Setup already complete' });
36
+ const { name, email, password } = req.body;
37
+ if (!name || !email || !password)
38
+ return res.status(400).json({ error: 'name, email and password are required' });
39
+ const user = await userService.createUser({ name, email, password, role: 'admin' });
40
+ res.status(201).json({ user });
41
+ } catch (e) {
42
+ next(e);
43
+ }
44
+ });
45
+
46
+ router.post('/login', async (req, res, next) => {
47
+ try {
48
+ const { email, password } = req.body;
49
+ if (!email || !password)
50
+ return res.status(400).json({ error: 'email and password are required' });
51
+ const result = await userService.login({ email, password });
52
+ if (!result) return res.status(401).json({ error: 'Invalid credentials' });
53
+ res.json(result);
54
+ } catch (e) {
55
+ next(e);
56
+ }
57
+ });
58
+
59
+ router.get('/me', jwtAuth, async (req, res, next) => {
60
+ try {
61
+ const user = await userService.getById(req.user.userId);
62
+ if (!user) return res.status(404).json({ error: 'User not found' });
63
+ res.json({ user });
64
+ } catch (e) {
65
+ next(e);
66
+ }
67
+ });
68
+
69
+ router.post('/change-password', jwtAuth, async (req, res, next) => {
70
+ try {
71
+ const { currentPassword, newPassword } = req.body;
72
+ if (!currentPassword || !newPassword)
73
+ return res.status(400).json({ error: 'currentPassword and newPassword are required' });
74
+ const result = await userService.updatePassword(req.user.userId, {
75
+ currentPassword,
76
+ newPassword
77
+ });
78
+ if (!result.ok) return res.status(400).json({ error: result.error });
79
+ res.json({ ok: true });
80
+ } catch (e) {
81
+ next(e);
82
+ }
83
+ });
84
+
85
+ router.put('/update-profile', jwtAuth, async (req, res, next) => {
86
+ try {
87
+ const { name, email } = req.body;
88
+ const result = await userService.updateProfile(req.user.userId, { name, email });
89
+ if (!result.ok) return res.status(400).json({ error: result.error });
90
+ res.json({ user: result.user });
91
+ } catch (e) {
92
+ next(e);
93
+ }
94
+ });
95
+
96
+ module.exports = router;
@@ -18,25 +18,61 @@
18
18
  const express = require('express');
19
19
  const router = express.Router();
20
20
  const settingsService = require('../services/settingsService');
21
+ const testSuiteService = require('../services/testSuiteService');
22
+ const testCaseService = require('../services/testCaseService');
23
+ const { jwtAuth } = require('../middleware/jwtAuth');
21
24
 
22
- router.get('/project', async (req, res) => {
25
+ router.get('/project', async (req, res, next) => {
23
26
  try {
24
27
  const project = await settingsService.getProject();
25
28
  res.json(project);
26
- } catch (error) {
27
- console.error('Error fetching project settings:', error);
28
- res.status(500).json({ error: 'Failed to fetch project settings' });
29
+ } catch (e) {
30
+ next(e);
29
31
  }
30
32
  });
31
33
 
32
- router.post('/project', async (req, res) => {
34
+ router.post('/project', async (req, res, next) => {
33
35
  try {
34
36
  const { name, logoUrl } = req.body;
35
37
  const project = await settingsService.updateProject({ name, logoUrl });
36
38
  res.json(project);
37
- } catch (error) {
38
- console.error('Error updating project settings:', error);
39
- res.status(500).json({ error: 'Failed to update project settings' });
39
+ } catch (e) {
40
+ next(e);
41
+ }
42
+ });
43
+
44
+ router.get('/test-prefixes', jwtAuth, async (req, res, next) => {
45
+ try {
46
+ const prefixes = await settingsService.getTestPrefixes();
47
+ res.json(prefixes);
48
+ } catch (e) {
49
+ next(e);
50
+ }
51
+ });
52
+
53
+ router.post('/test-prefixes', jwtAuth, async (req, res, next) => {
54
+ try {
55
+ const { testCasePrefix, testSuitePrefix } = req.body;
56
+ const project = await settingsService.updateTestPrefixes({ testCasePrefix, testSuitePrefix });
57
+ res.json(project);
58
+ } catch (e) {
59
+ next(e);
60
+ }
61
+ });
62
+
63
+ router.post('/test-prefixes/migrate', jwtAuth, async (req, res, next) => {
64
+ try {
65
+ const { testCasePrefix, testSuitePrefix } = req.body;
66
+ const results = {};
67
+ if (testCasePrefix) {
68
+ results.cases = await testCaseService.migratePrefix(testCasePrefix);
69
+ }
70
+ if (testSuitePrefix) {
71
+ results.suites = await testSuiteService.migratePrefix(testSuitePrefix);
72
+ }
73
+ res.json({ ok: true, ...results });
74
+ } catch (e) {
75
+ next(e);
40
76
  }
41
77
  });
42
78
 
@@ -0,0 +1,80 @@
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
+
18
+ const express = require('express');
19
+ const router = express.Router();
20
+ const { jwtAuth } = require('../middleware/jwtAuth');
21
+ const testCaseService = require('../services/testCaseService');
22
+
23
+ router.get('/:id', jwtAuth, async (req, res, next) => {
24
+ try {
25
+ const tc = await testCaseService.getById(req.params.id);
26
+ if (!tc) return res.status(404).json({ error: 'Test case not found' });
27
+ res.json({ testCase: tc });
28
+ } catch (e) {
29
+ next(e);
30
+ }
31
+ });
32
+
33
+ router.post('/', jwtAuth, async (req, res, next) => {
34
+ try {
35
+ const { suiteId, title, description, priority } = req.body;
36
+ if (!suiteId || !title)
37
+ return res.status(400).json({ error: 'suiteId and title are required' });
38
+ const testCase = await testCaseService.create({
39
+ suiteId,
40
+ title,
41
+ description,
42
+ priority,
43
+ createdById: req.user.userId
44
+ });
45
+ res.status(201).json({ testCase });
46
+ } catch (e) {
47
+ next(e);
48
+ }
49
+ });
50
+
51
+ router.put('/:id', jwtAuth, async (req, res, next) => {
52
+ try {
53
+ const { title, description, priority } = req.body;
54
+ const testCase = await testCaseService.update(req.params.id, { title, description, priority });
55
+ res.json({ testCase });
56
+ } catch (e) {
57
+ next(e);
58
+ }
59
+ });
60
+
61
+ router.put('/:id/steps', jwtAuth, async (req, res, next) => {
62
+ try {
63
+ const { steps } = req.body;
64
+ const saved = await testCaseService.upsertSteps(req.params.id, steps);
65
+ res.json({ steps: saved });
66
+ } catch (e) {
67
+ next(e);
68
+ }
69
+ });
70
+
71
+ router.delete('/:id', jwtAuth, async (req, res, next) => {
72
+ try {
73
+ await testCaseService.remove(req.params.id);
74
+ res.json({ ok: true });
75
+ } catch (e) {
76
+ next(e);
77
+ }
78
+ });
79
+
80
+ module.exports = router;
@@ -0,0 +1,122 @@
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
+
18
+ const express = require('express');
19
+ const router = express.Router();
20
+ const { jwtAuth } = require('../middleware/jwtAuth');
21
+ const testRunService = require('../services/testRunService');
22
+
23
+ router.get('/', jwtAuth, async (req, res, next) => {
24
+ try {
25
+ const page = Math.max(1, parseInt(req.query.page) || 1);
26
+ const limit = Math.min(200, Math.max(1, parseInt(req.query.limit) || 20));
27
+ const { q, sortBy, sortOrder } = req.query;
28
+ const result = await testRunService.getAll({ page, limit, q, sortBy, sortOrder });
29
+ res.json(result);
30
+ } catch (e) {
31
+ next(e);
32
+ }
33
+ });
34
+
35
+ router.get('/:id', jwtAuth, async (req, res, next) => {
36
+ try {
37
+ const run = await testRunService.getById(req.params.id);
38
+ if (!run) return res.status(404).json({ error: 'Test run not found' });
39
+ res.json({ run });
40
+ } catch (e) {
41
+ next(e);
42
+ }
43
+ });
44
+
45
+ router.post('/', jwtAuth, async (req, res, next) => {
46
+ try {
47
+ const { title, caseIds } = req.body;
48
+ if (!title) return res.status(400).json({ error: 'title is required' });
49
+ const run = await testRunService.create({ title, caseIds, createdById: req.user.userId });
50
+ res.status(201).json({ run });
51
+ } catch (e) {
52
+ next(e);
53
+ }
54
+ });
55
+
56
+ router.put('/:id', jwtAuth, async (req, res, next) => {
57
+ try {
58
+ const { title, status, caseIds } = req.body;
59
+ const run = await testRunService.update(req.params.id, { title, status, caseIds });
60
+ res.json({ run });
61
+ } catch (e) {
62
+ next(e);
63
+ }
64
+ });
65
+
66
+ router.post('/:id/duplicate', jwtAuth, async (req, res, next) => {
67
+ try {
68
+ const run = await testRunService.duplicate(req.params.id, { createdById: req.user.userId });
69
+ if (!run) return res.status(404).json({ error: 'Test run not found' });
70
+ res.status(201).json({ run });
71
+ } catch (e) {
72
+ next(e);
73
+ }
74
+ });
75
+
76
+ router.delete('/:id', jwtAuth, async (req, res, next) => {
77
+ try {
78
+ await testRunService.remove(req.params.id);
79
+ res.json({ ok: true });
80
+ } catch (e) {
81
+ next(e);
82
+ }
83
+ });
84
+
85
+ router.put('/entries/:entryId/assign', jwtAuth, async (req, res, next) => {
86
+ try {
87
+ const { userId } = req.body;
88
+ const entry = await testRunService.assignEntry(req.params.entryId, { userId: userId ?? null });
89
+ res.json({ entry });
90
+ } catch (e) {
91
+ next(e);
92
+ }
93
+ });
94
+
95
+ router.post('/entries/:entryId/result', jwtAuth, async (req, res, next) => {
96
+ try {
97
+ const { status, notes } = req.body;
98
+ if (!status) return res.status(400).json({ error: 'status is required' });
99
+ const entry = await testRunService.updateEntry(req.params.entryId, {
100
+ status,
101
+ notes,
102
+ executedById: req.user.userId
103
+ });
104
+ res.json({ entry });
105
+ } catch (e) {
106
+ next(e);
107
+ }
108
+ });
109
+
110
+ router.post('/:id/reorder', jwtAuth, async (req, res, next) => {
111
+ try {
112
+ const { entryIds } = req.body;
113
+ if (!Array.isArray(entryIds))
114
+ return res.status(400).json({ error: 'entryIds must be an array' });
115
+ await testRunService.reorderEntries(req.params.id, entryIds);
116
+ res.json({ ok: true });
117
+ } catch (e) {
118
+ next(e);
119
+ }
120
+ });
121
+
122
+ module.exports = router;
@@ -0,0 +1,92 @@
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
+
18
+ const express = require('express');
19
+ const router = express.Router();
20
+ const { jwtAuth } = require('../middleware/jwtAuth');
21
+ const testSuiteService = require('../services/testSuiteService');
22
+
23
+ router.get('/', jwtAuth, async (req, res, next) => {
24
+ try {
25
+ if (req.query.withCases === 'true') {
26
+ const suites = await testSuiteService.getAllWithCases();
27
+ return res.json({ suites });
28
+ }
29
+ if (req.query.q) {
30
+ const results = await testSuiteService.search(req.query.q);
31
+ return res.json(results);
32
+ }
33
+ const page = Math.max(1, parseInt(req.query.page) || 1);
34
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
35
+ const { sortBy, sortOrder } = req.query;
36
+ const result = await testSuiteService.getAll({ page, limit, sortBy, sortOrder });
37
+ res.json(result);
38
+ } catch (e) {
39
+ next(e);
40
+ }
41
+ });
42
+
43
+ router.get('/:id', jwtAuth, async (req, res, next) => {
44
+ try {
45
+ const suite = await testSuiteService.getById(req.params.id);
46
+ if (!suite) return res.status(404).json({ error: 'Suite not found' });
47
+ res.json({ suite });
48
+ } catch (e) {
49
+ next(e);
50
+ }
51
+ });
52
+
53
+ router.post('/', jwtAuth, async (req, res, next) => {
54
+ try {
55
+ const { name, description, priority } = req.body;
56
+ if (!name) return res.status(400).json({ error: 'name is required' });
57
+ const suite = await testSuiteService.create({
58
+ name,
59
+ description,
60
+ priority,
61
+ createdById: req.user.userId
62
+ });
63
+ res.status(201).json({ suite });
64
+ } catch (e) {
65
+ next(e);
66
+ }
67
+ });
68
+
69
+ router.put('/:id', jwtAuth, async (req, res, next) => {
70
+ try {
71
+ const { name, description, priority } = req.body;
72
+ const suite = await testSuiteService.update(req.params.id, {
73
+ name,
74
+ description,
75
+ priority
76
+ });
77
+ res.json({ suite });
78
+ } catch (e) {
79
+ next(e);
80
+ }
81
+ });
82
+
83
+ router.delete('/:id', jwtAuth, async (req, res, next) => {
84
+ try {
85
+ await testSuiteService.remove(req.params.id);
86
+ res.json({ ok: true });
87
+ } catch (e) {
88
+ next(e);
89
+ }
90
+ });
91
+
92
+ module.exports = router;
@@ -0,0 +1,67 @@
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
+
18
+ const express = require('express');
19
+ const router = express.Router();
20
+ const { jwtAuth } = require('../middleware/jwtAuth');
21
+ const { requireAdmin } = require('../middleware/requireAdmin');
22
+ const userService = require('../services/userService');
23
+
24
+ router.get('/members', jwtAuth, async (req, res, next) => {
25
+ try {
26
+ const users = await userService.getMembers();
27
+ res.json({ users });
28
+ } catch (e) {
29
+ next(e);
30
+ }
31
+ });
32
+
33
+ router.get('/', jwtAuth, requireAdmin, async (req, res, next) => {
34
+ try {
35
+ const users = await userService.getAll();
36
+ res.json({ users });
37
+ } catch (e) {
38
+ next(e);
39
+ }
40
+ });
41
+
42
+ router.post('/', jwtAuth, requireAdmin, async (req, res, next) => {
43
+ try {
44
+ const { name, email, password, role = 'user' } = req.body;
45
+ if (!name || !email || !password)
46
+ return res.status(400).json({ error: 'name, email and password are required' });
47
+ if (!['admin', 'user'].includes(role))
48
+ return res.status(400).json({ error: 'role must be admin or user' });
49
+ const user = await userService.createUser({ name, email, password, role });
50
+ res.status(201).json({ user });
51
+ } catch (e) {
52
+ next(e);
53
+ }
54
+ });
55
+
56
+ router.delete('/:id', jwtAuth, requireAdmin, async (req, res, next) => {
57
+ try {
58
+ if (req.params.id === req.user.userId)
59
+ return res.status(400).json({ error: 'Cannot delete your own account' });
60
+ await userService.deleteUser(req.params.id);
61
+ res.json({ ok: true });
62
+ } catch (e) {
63
+ next(e);
64
+ }
65
+ });
66
+
67
+ module.exports = router;
@@ -64,12 +64,13 @@ async function main() {
64
64
  }
65
65
 
66
66
  const pascal = toPascalCase(rawName);
67
+ const base = pascal.endsWith('Page') ? pascal.slice(0, -4) : pascal;
67
68
  const kebab = toKebabCase(pascal);
68
69
  const suiteTag = `@suite-${kebab}`;
69
70
  const testTag = `@test-${kebab}-1`;
70
71
 
71
72
  const featurePath = path.join(testsRoot, 'features', `${pascal}.feature`);
72
- const pagePath = path.join(testsRoot, 'pages', `${pascal}Page.ts`);
73
+ const pagePath = path.join(testsRoot, 'pages', `${base}Page.ts`);
73
74
  const stepsPath = path.join(testsRoot, 'step_definitions', `${pascal}Steps.ts`);
74
75
 
75
76
  const conflicts = [featurePath, pagePath, stepsPath].filter(fs.existsSync);
@@ -95,7 +96,7 @@ Feature: ${pascal}
95
96
 
96
97
  const page = `import { page } from '../utils/browser';
97
98
 
98
- export class ${pascal}Page {
99
+ export class ${base}Page {
99
100
  static async goTo() {
100
101
  await page().goto(process.env.BASE_URL as string);
101
102
  }
@@ -111,18 +112,18 @@ export class ${pascal}Page {
111
112
  `;
112
113
 
113
114
  const steps = `import { Given, When, Then } from '@cucumber/cucumber';
114
- import { ${pascal}Page } from '../pages/${pascal}Page';
115
+ import { ${base}Page } from '../pages/${base}Page';
115
116
 
116
117
  Given('I am on the ${pascal} page', async () => {
117
- await ${pascal}Page.goTo();
118
+ await ${base}Page.goTo();
118
119
  });
119
120
 
120
121
  When('I perform an action', async () => {
121
- await ${pascal}Page.performAction();
122
+ await ${base}Page.performAction();
122
123
  });
123
124
 
124
125
  Then('I should see the expected result', async () => {
125
- await ${pascal}Page.verifyResult();
126
+ await ${base}Page.verifyResult();
126
127
  });
127
128
  `;
128
129