plum-e2e 1.1.0 → 1.2.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 +27 -25
- package/.husky/pre-commit +2 -2
- package/README.md +142 -70
- package/backend/Dockerfile +4 -2
- package/backend/app.js +4 -2
- package/backend/config/scripts/generate-report.js +38 -30
- package/backend/entrypoint.sh +22 -0
- package/backend/package-lock.json +453 -10
- package/backend/package.json +5 -2
- package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
- package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +53 -0
- package/backend/routes/backup.routes.js +50 -0
- package/backend/routes/cron.routes.js +9 -60
- package/backend/routes/reports.routes.js +39 -6
- package/backend/routes/settings.routes.js +43 -0
- package/backend/server.js +52 -1
- package/backend/services/backupService.js +88 -0
- package/backend/services/cronService.js +68 -78
- package/backend/services/{scheduleService.js → prisma.js} +3 -15
- package/backend/services/reportService.js +44 -16
- package/backend/{routes/schedules.routes.js → services/settingsService.js} +17 -13
- package/bin/plum.js +213 -32
- package/docker-compose.yml +24 -0
- package/frontend/package-lock.json +2 -2
- package/frontend/package.json +1 -1
- package/frontend/src/lib/api/reports.js +38 -27
- package/frontend/src/lib/api/schedules.js +9 -25
- package/frontend/src/lib/api/settings.js +48 -0
- package/frontend/src/lib/components/layout/Nav.svelte +2 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
- package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
- package/frontend/src/lib/stores/runner.js +9 -0
- package/frontend/src/routes/+page.svelte +10 -3
- package/frontend/src/routes/reports/+page.svelte +342 -51
- package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
- package/frontend/src/routes/settings/+page.svelte +410 -0
- package/license-config.json +2 -2
- package/package.json +6 -2
- package/backend/config/scripts/create-settings.js +0 -53
package/backend/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plum-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"init": "node services/envService.js
|
|
6
|
+
"init": "node services/envService.js",
|
|
7
7
|
"create-step": "node config/scripts/create-step.mjs",
|
|
8
8
|
"create-env": "node services/envService.js",
|
|
9
9
|
"test": "node config/scripts/run-tests.js"
|
|
@@ -15,14 +15,17 @@
|
|
|
15
15
|
"@playwright/test": "^1.50.1",
|
|
16
16
|
"@types/node": "^22.17.0",
|
|
17
17
|
"cross-env": "^7.0.3",
|
|
18
|
+
"prisma": "^6.19.3",
|
|
18
19
|
"ts-node": "^10.9.2",
|
|
19
20
|
"typescript": "^5.9.2"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@clack/prompts": "^1.5.1",
|
|
23
24
|
"@cucumber/cucumber": "^11.2.0",
|
|
25
|
+
"@prisma/client": "^6.19.3",
|
|
24
26
|
"chai": "^4.3.6",
|
|
25
27
|
"chai-soft-assert": "^0.0.5",
|
|
28
|
+
"chokidar": "^5.0.0",
|
|
26
29
|
"cors": "^2.8.5",
|
|
27
30
|
"dotenv": "^16.4.7",
|
|
28
31
|
"express": "^4.21.2",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "CronJob" (
|
|
3
|
+
"id" SERIAL NOT NULL,
|
|
4
|
+
"taskName" TEXT NOT NULL,
|
|
5
|
+
"cronExpression" TEXT NOT NULL,
|
|
6
|
+
"tags" TEXT NOT NULL,
|
|
7
|
+
"workers" INTEGER NOT NULL DEFAULT 1,
|
|
8
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
10
|
+
|
|
11
|
+
CONSTRAINT "CronJob_pkey" PRIMARY KEY ("id")
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
-- CreateTable
|
|
15
|
+
CREATE TABLE "Report" (
|
|
16
|
+
"id" SERIAL NOT NULL,
|
|
17
|
+
"fileName" TEXT NOT NULL,
|
|
18
|
+
"status" TEXT NOT NULL,
|
|
19
|
+
"tags" TEXT NOT NULL,
|
|
20
|
+
"triggerType" TEXT NOT NULL DEFAULT 'manual-trigger',
|
|
21
|
+
"runners" INTEGER NOT NULL DEFAULT 1,
|
|
22
|
+
"cronJobId" INTEGER,
|
|
23
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
|
|
25
|
+
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- CreateIndex
|
|
29
|
+
CREATE UNIQUE INDEX "CronJob_taskName_key" ON "CronJob"("taskName");
|
|
30
|
+
|
|
31
|
+
-- CreateIndex
|
|
32
|
+
CREATE UNIQUE INDEX "Report_fileName_key" ON "Report"("fileName");
|
|
33
|
+
|
|
34
|
+
-- AddForeignKey
|
|
35
|
+
ALTER TABLE "Report" ADD CONSTRAINT "Report_cronJobId_fkey" FOREIGN KEY ("cronJobId") REFERENCES "CronJob"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
generator client {
|
|
18
|
+
provider = "prisma-client-js"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
datasource db {
|
|
22
|
+
provider = "postgresql"
|
|
23
|
+
url = env("DATABASE_URL")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model CronJob {
|
|
27
|
+
id Int @id @default(autoincrement())
|
|
28
|
+
taskName String @unique
|
|
29
|
+
cronExpression String
|
|
30
|
+
tags String
|
|
31
|
+
workers Int @default(1)
|
|
32
|
+
createdAt DateTime @default(now())
|
|
33
|
+
updatedAt DateTime @updatedAt
|
|
34
|
+
reports Report[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
model Report {
|
|
38
|
+
id Int @id @default(autoincrement())
|
|
39
|
+
fileName String @unique
|
|
40
|
+
status String
|
|
41
|
+
tags String
|
|
42
|
+
triggerType String @default("manual-trigger")
|
|
43
|
+
runners Int @default(1)
|
|
44
|
+
cronJobId Int?
|
|
45
|
+
cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
|
|
46
|
+
createdAt DateTime @default(now())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
model Project {
|
|
50
|
+
id Int @id @default(autoincrement())
|
|
51
|
+
name String @default("")
|
|
52
|
+
logoUrl String @default("")
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 backupService = require('../services/backupService');
|
|
21
|
+
const cronService = require('../services/cronService');
|
|
22
|
+
|
|
23
|
+
router.get('/export', async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const data = await backupService.exportAll();
|
|
26
|
+
const fileName = `plum-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
|
27
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
28
|
+
res.setHeader('Content-Type', 'application/json');
|
|
29
|
+
res.json(data);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Export failed:', error);
|
|
32
|
+
res.status(500).json({ error: 'Export failed' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
router.post('/import', async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const { cronJobs, reports, project } = req.body;
|
|
39
|
+
if (!Array.isArray(cronJobs) && !Array.isArray(reports) && !project) {
|
|
40
|
+
return res.status(400).json({ error: 'Invalid backup format' });
|
|
41
|
+
}
|
|
42
|
+
await backupService.importAll({ cronJobs, reports, project }, cronService);
|
|
43
|
+
res.json({ message: 'Import successful' });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Import failed:', error);
|
|
46
|
+
res.status(500).json({ error: 'Import failed' });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
module.exports = router;
|
|
@@ -19,14 +19,9 @@ const express = require('express');
|
|
|
19
19
|
const router = express.Router();
|
|
20
20
|
const cronService = require('../services/cronService');
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
* Get Cron Jobs
|
|
24
|
-
* Description:
|
|
25
|
-
* - Get all cron jobs from config/cron-jobs.json
|
|
26
|
-
* ------------------------------------------------------ */
|
|
27
|
-
router.get('/', (req, res) => {
|
|
22
|
+
router.get('/', async (req, res) => {
|
|
28
23
|
try {
|
|
29
|
-
const cronJobs = cronService.getAllCronJobs();
|
|
24
|
+
const cronJobs = await cronService.getAllCronJobs();
|
|
30
25
|
res.json({ cronJobs });
|
|
31
26
|
} catch (error) {
|
|
32
27
|
console.error('Error fetching cron jobs:', error);
|
|
@@ -34,28 +29,13 @@ router.get('/', (req, res) => {
|
|
|
34
29
|
}
|
|
35
30
|
});
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
* Create Cron Job
|
|
39
|
-
* Description:
|
|
40
|
-
* Add a new cron job to config/cron-jobs.json
|
|
41
|
-
* Params:
|
|
42
|
-
* - cronExpression:
|
|
43
|
-
* e.g. "* * * * *"
|
|
44
|
-
* https://www.baeldung.com/cron-expressions
|
|
45
|
-
* - taskName:
|
|
46
|
-
* the unique identifier
|
|
47
|
-
* - tags:
|
|
48
|
-
* cucumber tag you want to run when cron job
|
|
49
|
-
* is triggered.
|
|
50
|
-
* ------------------------------------------------------ */
|
|
51
|
-
router.post('/', (req, res) => {
|
|
32
|
+
router.post('/', async (req, res) => {
|
|
52
33
|
try {
|
|
53
34
|
const { cronExpression, taskName, tags } = req.body;
|
|
54
35
|
if (!cronExpression || !taskName || !tags) {
|
|
55
36
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
56
37
|
}
|
|
57
|
-
|
|
58
|
-
cronService.addCronJob(req.body);
|
|
38
|
+
await cronService.addCronJob(req.body);
|
|
59
39
|
res.json({
|
|
60
40
|
message: `Cron job ${taskName} added with tags: ${tags}`,
|
|
61
41
|
taskName,
|
|
@@ -67,56 +47,25 @@ router.post('/', (req, res) => {
|
|
|
67
47
|
}
|
|
68
48
|
});
|
|
69
49
|
|
|
70
|
-
|
|
71
|
-
* Edit Cron Job
|
|
72
|
-
* Description:
|
|
73
|
-
* Edit an existing cron job from
|
|
74
|
-
* config/cron-jobs.json
|
|
75
|
-
* Params:
|
|
76
|
-
* - taskName:
|
|
77
|
-
* the unique identifier
|
|
78
|
-
* - cronExpression:
|
|
79
|
-
* e.g. "* * * * *"
|
|
80
|
-
* https://www.baeldung.com/cron-expressions
|
|
81
|
-
* - tags:
|
|
82
|
-
* cucumber tag you want to run when cron job
|
|
83
|
-
* is triggered.
|
|
84
|
-
* ------------------------------------------------------ */
|
|
85
|
-
router.put('/:taskName', (req, res) => {
|
|
50
|
+
router.put('/:taskName', async (req, res) => {
|
|
86
51
|
try {
|
|
87
52
|
const { taskName } = req.params;
|
|
88
53
|
const { cronExpression, tags } = req.body;
|
|
89
|
-
|
|
90
54
|
if (!cronExpression || !tags) {
|
|
91
55
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
92
56
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
res.json({
|
|
96
|
-
message: `Cron job ${taskName} updated`,
|
|
97
|
-
taskName,
|
|
98
|
-
cronExpression,
|
|
99
|
-
tags: updatedCronJob.tags
|
|
100
|
-
});
|
|
57
|
+
await cronService.updateCronJob(taskName, req.body);
|
|
58
|
+
res.json({ message: `Cron job ${taskName} updated`, taskName, cronExpression, tags });
|
|
101
59
|
} catch (error) {
|
|
102
60
|
console.error('Error updating cron job:', error);
|
|
103
61
|
res.status(500).json({ error: 'Failed to update cron job' });
|
|
104
62
|
}
|
|
105
63
|
});
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
* Delete Cron Job
|
|
109
|
-
* Description:
|
|
110
|
-
* Delete cron job from config/cron-jobs.json
|
|
111
|
-
* by taskName
|
|
112
|
-
* Params:
|
|
113
|
-
* - taskName:
|
|
114
|
-
* the unique identifier
|
|
115
|
-
* ------------------------------------------------------ */
|
|
116
|
-
router.delete('/:taskName', (req, res) => {
|
|
65
|
+
router.delete('/:taskName', async (req, res) => {
|
|
117
66
|
try {
|
|
118
67
|
const { taskName } = req.params;
|
|
119
|
-
cronService.removeCronJob(taskName);
|
|
68
|
+
await cronService.removeCronJob(taskName);
|
|
120
69
|
res.json({ message: `Cron job ${taskName} deleted` });
|
|
121
70
|
} catch (error) {
|
|
122
71
|
console.error('Error deleting cron job:', error);
|
|
@@ -19,14 +19,24 @@ const express = require('express');
|
|
|
19
19
|
const router = express.Router();
|
|
20
20
|
const reportService = require('../services/reportService');
|
|
21
21
|
|
|
22
|
-
router.get('/', (req, res) => {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
router.get('/', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const reports = await reportService.getAllReports();
|
|
25
|
+
res.json({ reports });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Error fetching reports:', error);
|
|
28
|
+
res.status(500).json({ error: 'Failed to fetch reports' });
|
|
29
|
+
}
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
router.get('/latest', (req, res) => {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
router.get('/latest', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const latestReport = await reportService.getLatestReport();
|
|
35
|
+
res.json({ latestReport });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error fetching latest report:', error);
|
|
38
|
+
res.status(500).json({ error: 'Failed to fetch latest report' });
|
|
39
|
+
}
|
|
30
40
|
});
|
|
31
41
|
|
|
32
42
|
router.get('/:fileName/detail', (req, res) => {
|
|
@@ -35,4 +45,27 @@ router.get('/:fileName/detail', (req, res) => {
|
|
|
35
45
|
res.json(detail);
|
|
36
46
|
});
|
|
37
47
|
|
|
48
|
+
router.delete('/bulk', async (req, res) => {
|
|
49
|
+
const { fileNames } = req.body;
|
|
50
|
+
if (!Array.isArray(fileNames) || fileNames.length === 0)
|
|
51
|
+
return res.status(400).json({ error: 'fileNames array required' });
|
|
52
|
+
try {
|
|
53
|
+
await reportService.deleteReports(fileNames);
|
|
54
|
+
res.json({ deleted: fileNames.length });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error deleting reports:', error);
|
|
57
|
+
res.status(500).json({ error: 'Failed to delete reports' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
router.delete('/:fileName', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
await reportService.deleteReport(req.params.fileName);
|
|
64
|
+
res.json({ deleted: req.params.fileName });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error deleting report:', error);
|
|
67
|
+
res.status(500).json({ error: 'Failed to delete report' });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
38
71
|
module.exports = router;
|
|
@@ -0,0 +1,43 @@
|
|
|
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 settingsService = require('../services/settingsService');
|
|
21
|
+
|
|
22
|
+
router.get('/project', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const project = await settingsService.getProject();
|
|
25
|
+
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
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/project', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const { name, logoUrl } = req.body;
|
|
35
|
+
const project = await settingsService.updateProject({ name, logoUrl });
|
|
36
|
+
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' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
module.exports = router;
|
package/backend/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const http = require('http');
|
|
|
19
19
|
const { Server } = require('socket.io');
|
|
20
20
|
const app = require('./app');
|
|
21
21
|
const socketHandler = require('./websockets/socketHandler.js');
|
|
22
|
+
const cronService = require('./services/cronService');
|
|
22
23
|
const server = http.createServer(app);
|
|
23
24
|
const io = new Server(server, { cors: { origin: '*' } });
|
|
24
25
|
const path = require('path');
|
|
@@ -35,5 +36,55 @@ if (!fs.existsSync(testsDir)) {
|
|
|
35
36
|
console.log('📂 Loading tests from:', testsDir);
|
|
36
37
|
|
|
37
38
|
socketHandler(io);
|
|
39
|
+
cronService.setSocketIO(io);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
async function start() {
|
|
42
|
+
await cronService.init();
|
|
43
|
+
server.listen(3001, async () => {
|
|
44
|
+
console.log('Backend running on port 3001');
|
|
45
|
+
|
|
46
|
+
// chokidar v5+ is ESM-only — use dynamic import to stay compatible with CJS
|
|
47
|
+
let chokidar;
|
|
48
|
+
try {
|
|
49
|
+
chokidar = (await import('chokidar')).default;
|
|
50
|
+
} catch {
|
|
51
|
+
console.warn('⚠️ chokidar unavailable — file watching disabled');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const watchOpts = { usePolling: true, interval: 800, ignoreInitial: true };
|
|
56
|
+
|
|
57
|
+
// Watch tests/features/ — notify UI when feature files are added/changed/removed
|
|
58
|
+
const featuresDir = path.join(testsDir, 'features');
|
|
59
|
+
if (fs.existsSync(featuresDir)) {
|
|
60
|
+
let debounce = null;
|
|
61
|
+
chokidar.watch(featuresDir, watchOpts).on('all', (event, filePath) => {
|
|
62
|
+
clearTimeout(debounce);
|
|
63
|
+
debounce = setTimeout(() => {
|
|
64
|
+
console.log(
|
|
65
|
+
`📝 Tests changed (${event}: ${path.basename(filePath)}) — notifying clients`
|
|
66
|
+
);
|
|
67
|
+
io.emit('tests-changed');
|
|
68
|
+
}, 300);
|
|
69
|
+
});
|
|
70
|
+
console.log('👀 Watching for test file changes...');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Watch reports/ — notify UI when a new report file lands
|
|
74
|
+
const reportsDir = path.resolve(process.cwd(), 'reports');
|
|
75
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
76
|
+
chokidar.watch(reportsDir, { ...watchOpts, interval: 1200 }).on('add', (filePath) => {
|
|
77
|
+
const name = path.basename(filePath);
|
|
78
|
+
if ((name.startsWith('PASS_') || name.startsWith('FAIL_')) && name.endsWith('.json')) {
|
|
79
|
+
console.log(`📊 New report: ${name} — notifying clients`);
|
|
80
|
+
io.emit('report-ready');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
console.log('👀 Watching for new reports...');
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
start().catch((err) => {
|
|
88
|
+
console.error('Failed to start server:', err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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 prisma = require('./prisma');
|
|
19
|
+
|
|
20
|
+
const exportAll = async () => {
|
|
21
|
+
const [cronJobs, reports, project] = await Promise.all([
|
|
22
|
+
prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } }),
|
|
23
|
+
prisma.report.findMany({
|
|
24
|
+
orderBy: { createdAt: 'asc' },
|
|
25
|
+
include: { cronJob: { select: { taskName: true } } }
|
|
26
|
+
}),
|
|
27
|
+
prisma.project.findUnique({ where: { id: 1 } })
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Annotate each report with the cronJob's taskName for portable restoration
|
|
31
|
+
const portableReports = reports.map(({ cronJob, cronJobId, ...r }) => ({
|
|
32
|
+
...r,
|
|
33
|
+
cronJobTaskName: cronJob?.taskName ?? null
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
version: '1',
|
|
38
|
+
exportedAt: new Date().toISOString(),
|
|
39
|
+
cronJobs: cronJobs.map(({ id, createdAt, updatedAt, reports: _, ...r }) => r),
|
|
40
|
+
reports: portableReports.map(({ id, createdAt, ...r }) => r),
|
|
41
|
+
project: project ? { name: project.name, logoUrl: project.logoUrl } : null
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const importAll = async ({ cronJobs = [], reports = [], project = null }, cronService) => {
|
|
46
|
+
await prisma.$transaction(async (tx) => {
|
|
47
|
+
// Upsert cron jobs and build taskName → id map
|
|
48
|
+
const taskNameToId = {};
|
|
49
|
+
for (const job of cronJobs) {
|
|
50
|
+
const upserted = await tx.cronJob.upsert({
|
|
51
|
+
where: { taskName: job.taskName },
|
|
52
|
+
create: {
|
|
53
|
+
taskName: job.taskName,
|
|
54
|
+
cronExpression: job.cronExpression,
|
|
55
|
+
tags: job.tags,
|
|
56
|
+
workers: job.workers ?? 1
|
|
57
|
+
},
|
|
58
|
+
update: { cronExpression: job.cronExpression, tags: job.tags, workers: job.workers ?? 1 }
|
|
59
|
+
});
|
|
60
|
+
taskNameToId[job.taskName] = upserted.id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Upsert reports, resolving cronJobId from the taskName map
|
|
64
|
+
for (const report of reports) {
|
|
65
|
+
const { cronJobTaskName, ...data } = report;
|
|
66
|
+
const cronJobId = cronJobTaskName ? (taskNameToId[cronJobTaskName] ?? null) : null;
|
|
67
|
+
await tx.report.upsert({
|
|
68
|
+
where: { fileName: data.fileName },
|
|
69
|
+
create: { ...data, cronJobId },
|
|
70
|
+
update: { ...data, cronJobId }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Restore project settings
|
|
75
|
+
if (project) {
|
|
76
|
+
await tx.project.upsert({
|
|
77
|
+
where: { id: 1 },
|
|
78
|
+
create: { id: 1, name: project.name ?? '', logoUrl: project.logoUrl ?? '' },
|
|
79
|
+
update: { name: project.name ?? '', logoUrl: project.logoUrl ?? '' }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Re-schedule cron jobs after import
|
|
85
|
+
if (cronService) await cronService.reload();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = { exportAll, importAll };
|