plum-e2e 1.3.6 → 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.
- package/README.md +61 -3
- package/backend/app.js +5 -0
- package/backend/config/scripts/create-test.mjs +172 -0
- package/backend/config/scripts/generate-report.js +2 -1
- package/backend/middleware/jwtAuth.js +33 -0
- package/backend/middleware/requireAdmin.js +25 -0
- package/backend/package.json +2 -0
- package/backend/prisma/migrations/20260618000000_add_test_repository/migration.sql +133 -0
- package/backend/prisma/migrations/20260618000001_add_user_roles/migration.sql +3 -0
- package/backend/prisma/migrations/20260618000002_drop_automated_tag/migration.sql +2 -0
- package/backend/prisma/migrations/20260618000003_entry_assignee/migration.sql +2 -0
- package/backend/prisma/schema.prisma +118 -10
- package/backend/routes/auth.routes.js +96 -0
- package/backend/routes/settings.routes.js +44 -8
- package/backend/routes/test-cases.routes.js +80 -0
- package/backend/routes/test-runs.routes.js +122 -0
- package/backend/routes/test-suites.routes.js +92 -0
- package/backend/routes/users.routes.js +67 -0
- package/backend/scripts/create-test.js +7 -6
- package/backend/services/reportService.js +96 -4
- package/backend/services/runnerService.js +16 -1
- package/backend/services/settingsService.js +18 -2
- package/backend/services/testCaseService.js +139 -0
- package/backend/services/testRunService.js +203 -0
- package/backend/services/testSuiteService.js +191 -0
- package/backend/services/userService.js +114 -0
- package/backend/websockets/socketHandler.js +19 -6
- package/bin/plum.js +105 -9
- package/frontend/src/lib/api/auth.js +69 -0
- package/frontend/src/lib/api/repository.js +256 -0
- package/frontend/src/lib/api/users.js +52 -0
- package/frontend/src/lib/components/layout/Nav.svelte +116 -4
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +243 -29
- package/frontend/src/lib/components/ui/Modal.svelte +8 -1
- package/frontend/src/lib/constants.js +2 -0
- package/frontend/src/lib/stores/auth.js +60 -0
- package/frontend/src/lib/stores/runner.js +9 -2
- package/frontend/src/routes/+layout.svelte +32 -4
- package/frontend/src/routes/+page.svelte +1 -1
- package/frontend/src/routes/login/+page.svelte +209 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +3 -1
- package/frontend/src/routes/settings/+page.svelte +586 -5
- package/frontend/src/routes/setup/+page.svelte +249 -0
- package/frontend/src/routes/test-repository/+page.svelte +1379 -0
- package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +1549 -0
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +1490 -0
- package/package.json +1 -1
|
@@ -22,6 +22,85 @@ const prisma = require('./prisma');
|
|
|
22
22
|
const { isScheduledTrigger, normaliseTrigger } = require('../constants/triggers');
|
|
23
23
|
const { SCREENSHOTS_DIR } = require('../lib/reportFilename');
|
|
24
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Auto-sync: mark test cases as automated and record history from Cucumber tags
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
async function syncAutomatedTags(reportId, features, testRunId = null) {
|
|
30
|
+
try {
|
|
31
|
+
const tagSet = new Set();
|
|
32
|
+
for (const feature of features) {
|
|
33
|
+
for (const scenario of feature.scenarios ?? []) {
|
|
34
|
+
for (const tag of scenario.tags ?? []) {
|
|
35
|
+
tagSet.add(tag.replace(/^@/, ''));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (tagSet.size === 0) return;
|
|
40
|
+
|
|
41
|
+
const matchingCases = await prisma.testCase.findMany({
|
|
42
|
+
where: { displayId: { in: [...tagSet] } },
|
|
43
|
+
select: { id: true, displayId: true }
|
|
44
|
+
});
|
|
45
|
+
if (matchingCases.length === 0) return;
|
|
46
|
+
|
|
47
|
+
const tagToResult = new Map();
|
|
48
|
+
for (const feature of features) {
|
|
49
|
+
for (const scenario of feature.scenarios ?? []) {
|
|
50
|
+
for (const tag of scenario.tags ?? []) {
|
|
51
|
+
const t = tag.replace(/^@/, '');
|
|
52
|
+
const result = scenario.status === 'passed' ? 'pass' : 'fail';
|
|
53
|
+
if (!tagToResult.has(t) || result === 'fail') {
|
|
54
|
+
tagToResult.set(t, result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await prisma.$transaction([
|
|
61
|
+
...matchingCases.map((tc) =>
|
|
62
|
+
prisma.testCase.update({ where: { id: tc.id }, data: { isAutomated: true } })
|
|
63
|
+
),
|
|
64
|
+
...matchingCases.map((tc) =>
|
|
65
|
+
prisma.testCaseHistory.create({
|
|
66
|
+
data: {
|
|
67
|
+
caseId: tc.id,
|
|
68
|
+
reportId,
|
|
69
|
+
result: tagToResult.get(tc.displayId) ?? 'pass',
|
|
70
|
+
source: 'automated'
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
if (testRunId) {
|
|
77
|
+
const entries = await prisma.testRunEntry.findMany({
|
|
78
|
+
where: { runId: testRunId, case: { displayId: { in: [...tagToResult.keys()] } } },
|
|
79
|
+
select: { id: true, case: { select: { displayId: true } } }
|
|
80
|
+
});
|
|
81
|
+
if (entries.length > 0) {
|
|
82
|
+
await prisma.$transaction([
|
|
83
|
+
...entries.map((e) =>
|
|
84
|
+
prisma.testRunEntry.update({
|
|
85
|
+
where: { id: e.id },
|
|
86
|
+
data: {
|
|
87
|
+
status: tagToResult.get(e.case.displayId) ?? 'pass',
|
|
88
|
+
executedAt: new Date()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
),
|
|
92
|
+
prisma.testRun.updateMany({
|
|
93
|
+
where: { id: testRunId, status: { in: ['backlog', 'draft'] } },
|
|
94
|
+
data: { status: 'in-progress' }
|
|
95
|
+
})
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error('[sync] Failed to sync automated tags:', e.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
25
104
|
// ---------------------------------------------------------------------------
|
|
26
105
|
// Internal helpers
|
|
27
106
|
// ---------------------------------------------------------------------------
|
|
@@ -207,6 +286,7 @@ const getReportDetail = async (id) => {
|
|
|
207
286
|
* browser?: string,
|
|
208
287
|
* runnerName?: string,
|
|
209
288
|
* runnerId?: string,
|
|
289
|
+
* testRunId?: string,
|
|
210
290
|
* }} opts
|
|
211
291
|
*/
|
|
212
292
|
const saveReport = async ({
|
|
@@ -216,13 +296,14 @@ const saveReport = async ({
|
|
|
216
296
|
nodeCount,
|
|
217
297
|
browser,
|
|
218
298
|
runnerName,
|
|
219
|
-
runnerId
|
|
299
|
+
runnerId,
|
|
300
|
+
testRunId
|
|
220
301
|
}) => {
|
|
221
302
|
const normTrigger = normaliseTrigger(triggerType);
|
|
222
303
|
const { features, status } = processCucumberJson(rawCucumberJson);
|
|
223
304
|
const cronJobId = await resolveCronJobId(normTrigger);
|
|
224
305
|
|
|
225
|
-
|
|
306
|
+
const report = await prisma.report.create({
|
|
226
307
|
data: {
|
|
227
308
|
status,
|
|
228
309
|
tags: (tags ?? '').replace(/^\(|\)$/g, '') || '@all-tests',
|
|
@@ -235,6 +316,8 @@ const saveReport = async ({
|
|
|
235
316
|
content: { features }
|
|
236
317
|
}
|
|
237
318
|
});
|
|
319
|
+
syncAutomatedTags(report.id, features, testRunId ?? null);
|
|
320
|
+
return report;
|
|
238
321
|
};
|
|
239
322
|
|
|
240
323
|
/**
|
|
@@ -250,7 +333,15 @@ const saveReport = async ({
|
|
|
250
333
|
* browser: string,
|
|
251
334
|
* }} opts
|
|
252
335
|
*/
|
|
253
|
-
const saveCombinedReport = async ({
|
|
336
|
+
const saveCombinedReport = async ({
|
|
337
|
+
reports,
|
|
338
|
+
runners,
|
|
339
|
+
overallCode,
|
|
340
|
+
tag,
|
|
341
|
+
triggerType,
|
|
342
|
+
browser,
|
|
343
|
+
testRunId
|
|
344
|
+
}) => {
|
|
254
345
|
const featureMap = new Map();
|
|
255
346
|
for (const content of reports) {
|
|
256
347
|
if (!content) continue;
|
|
@@ -282,7 +373,8 @@ const saveCombinedReport = async ({ reports, runners, overallCode, tag, triggerT
|
|
|
282
373
|
nodeCount: runners.length,
|
|
283
374
|
browser,
|
|
284
375
|
runnerName: runners.map((r) => r.name).join(', '),
|
|
285
|
-
runnerId: null
|
|
376
|
+
runnerId: null,
|
|
377
|
+
testRunId: testRunId ?? null
|
|
286
378
|
});
|
|
287
379
|
};
|
|
288
380
|
|
|
@@ -28,7 +28,22 @@ const getAll = () => prisma.runner.findMany({ orderBy: { createdAt: 'asc' } });
|
|
|
28
28
|
const create = ({ name, url, token, browser = 'chromium' }) =>
|
|
29
29
|
prisma.runner.create({ data: { name, url, token, browser } });
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
async function remove(id) {
|
|
32
|
+
// Scrub the deleted runner from any cron job's runnerIds string before
|
|
33
|
+
// deleting, since that field has no relational constraint.
|
|
34
|
+
const jobs = await prisma.cronJob.findMany({ select: { id: true, runnerIds: true } });
|
|
35
|
+
for (const job of jobs) {
|
|
36
|
+
const ids = job.runnerIds
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter((s) => s && s !== id);
|
|
40
|
+
await prisma.cronJob.update({
|
|
41
|
+
where: { id: job.id },
|
|
42
|
+
data: { runnerIds: ids.length > 0 ? ids.join(',') : 'built-in' }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return prisma.runner.delete({ where: { id } });
|
|
46
|
+
}
|
|
32
47
|
|
|
33
48
|
const update = (id, data) => prisma.runner.update({ where: { id }, data });
|
|
34
49
|
|
|
@@ -20,7 +20,7 @@ const prisma = require('./prisma');
|
|
|
20
20
|
const getProject = async () => {
|
|
21
21
|
let project = await prisma.project.findUnique({ where: { id: 1 } });
|
|
22
22
|
if (!project) {
|
|
23
|
-
project = await prisma.project.create({ data: { id: 1
|
|
23
|
+
project = await prisma.project.create({ data: { id: 1 } });
|
|
24
24
|
}
|
|
25
25
|
return project;
|
|
26
26
|
};
|
|
@@ -33,4 +33,20 @@ const updateProject = async ({ name, logoUrl }) => {
|
|
|
33
33
|
});
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const getTestPrefixes = async () => {
|
|
37
|
+
const project = await getProject();
|
|
38
|
+
return { testCasePrefix: project.testCasePrefix, testSuitePrefix: project.testSuitePrefix };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const updateTestPrefixes = async ({ testCasePrefix, testSuitePrefix }) => {
|
|
42
|
+
return prisma.project.upsert({
|
|
43
|
+
where: { id: 1 },
|
|
44
|
+
create: { id: 1 },
|
|
45
|
+
update: {
|
|
46
|
+
...(testCasePrefix !== undefined && { testCasePrefix }),
|
|
47
|
+
...(testSuitePrefix !== undefined && { testSuitePrefix })
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = { getProject, updateProject, getTestPrefixes, updateTestPrefixes };
|
|
@@ -0,0 +1,139 @@
|
|
|
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 caseSelect = {
|
|
21
|
+
id: true,
|
|
22
|
+
displayId: true,
|
|
23
|
+
title: true,
|
|
24
|
+
description: true,
|
|
25
|
+
priority: true,
|
|
26
|
+
isAutomated: true,
|
|
27
|
+
suiteId: true,
|
|
28
|
+
createdAt: true,
|
|
29
|
+
updatedAt: true,
|
|
30
|
+
createdBy: { select: { id: true, name: true } },
|
|
31
|
+
suite: { select: { id: true, displayId: true, name: true } },
|
|
32
|
+
_count: { select: { steps: true } }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function getById(id) {
|
|
36
|
+
return prisma.testCase.findUnique({
|
|
37
|
+
where: { id },
|
|
38
|
+
select: {
|
|
39
|
+
...caseSelect,
|
|
40
|
+
steps: {
|
|
41
|
+
orderBy: { order: 'asc' }
|
|
42
|
+
},
|
|
43
|
+
history: {
|
|
44
|
+
select: {
|
|
45
|
+
id: true,
|
|
46
|
+
result: true,
|
|
47
|
+
source: true,
|
|
48
|
+
notes: true,
|
|
49
|
+
executedAt: true,
|
|
50
|
+
executedBy: { select: { id: true, name: true } },
|
|
51
|
+
run: { select: { id: true, title: true } },
|
|
52
|
+
report: { select: { id: true, status: true, createdAt: true } }
|
|
53
|
+
},
|
|
54
|
+
orderBy: { executedAt: 'desc' },
|
|
55
|
+
take: 50
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function create({ suiteId, title, description, priority, createdById }) {
|
|
62
|
+
const project = await prisma.project.upsert({
|
|
63
|
+
where: { id: 1 },
|
|
64
|
+
create: { id: 1, caseSeqNext: 1 },
|
|
65
|
+
update: { caseSeqNext: { increment: 1 } },
|
|
66
|
+
select: { caseSeqNext: true, testCasePrefix: true }
|
|
67
|
+
});
|
|
68
|
+
const num = String(project.caseSeqNext).padStart(3, '0');
|
|
69
|
+
const displayId = `${project.testCasePrefix}-${num}`;
|
|
70
|
+
|
|
71
|
+
return prisma.testCase.create({
|
|
72
|
+
data: {
|
|
73
|
+
displayId,
|
|
74
|
+
suiteId,
|
|
75
|
+
title,
|
|
76
|
+
description: description ?? '',
|
|
77
|
+
priority: priority ?? 'Medium',
|
|
78
|
+
createdById
|
|
79
|
+
},
|
|
80
|
+
select: caseSelect
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function update(id, { title, description, priority }) {
|
|
85
|
+
return prisma.testCase.update({
|
|
86
|
+
where: { id },
|
|
87
|
+
data: {
|
|
88
|
+
...(title !== undefined && { title }),
|
|
89
|
+
...(description !== undefined && { description }),
|
|
90
|
+
...(priority !== undefined && { priority })
|
|
91
|
+
},
|
|
92
|
+
select: caseSelect
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function remove(id) {
|
|
97
|
+
return prisma.testCase.delete({ where: { id } });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function upsertSteps(caseId, steps) {
|
|
101
|
+
await prisma.testStep.deleteMany({ where: { caseId } });
|
|
102
|
+
if (!steps || steps.length === 0) return [];
|
|
103
|
+
return prisma.$transaction(
|
|
104
|
+
steps.map((step, i) =>
|
|
105
|
+
prisma.testStep.create({
|
|
106
|
+
data: {
|
|
107
|
+
caseId,
|
|
108
|
+
action: step.action ?? '',
|
|
109
|
+
testData: step.testData ?? '',
|
|
110
|
+
expectedOutput: step.expectedOutput ?? '',
|
|
111
|
+
order: i
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function migratePrefix(newPrefix) {
|
|
119
|
+
const cases = await prisma.testCase.findMany({
|
|
120
|
+
select: { id: true, displayId: true },
|
|
121
|
+
orderBy: { createdAt: 'asc' }
|
|
122
|
+
});
|
|
123
|
+
const project = await prisma.project.upsert({
|
|
124
|
+
where: { id: 1 },
|
|
125
|
+
create: { id: 1 },
|
|
126
|
+
update: { testCasePrefix: newPrefix },
|
|
127
|
+
select: { testCasePrefix: true }
|
|
128
|
+
});
|
|
129
|
+
for (let i = 0; i < cases.length; i++) {
|
|
130
|
+
const num = String(i + 1).padStart(3, '0');
|
|
131
|
+
await prisma.testCase.update({
|
|
132
|
+
where: { id: cases[i].id },
|
|
133
|
+
data: { displayId: `${newPrefix}-${num}` }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return project;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { getById, create, update, remove, upsertSteps, migratePrefix };
|
|
@@ -0,0 +1,203 @@
|
|
|
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 runListSelect = {
|
|
21
|
+
id: true,
|
|
22
|
+
title: true,
|
|
23
|
+
status: true,
|
|
24
|
+
createdAt: true,
|
|
25
|
+
updatedAt: true,
|
|
26
|
+
createdBy: { select: { id: true, name: true } },
|
|
27
|
+
_count: { select: { entries: true } }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function runOrderBy(sortBy, sortOrder) {
|
|
31
|
+
const dir = sortOrder === 'desc' ? 'desc' : 'asc';
|
|
32
|
+
if (sortBy === 'title') return { title: dir };
|
|
33
|
+
if (sortBy === 'status') return { status: dir };
|
|
34
|
+
if (sortBy === 'updatedAt') return { updatedAt: dir };
|
|
35
|
+
return { createdAt: dir };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getAll({ page = 1, limit = 20, q, sortBy = 'createdAt', sortOrder = 'desc' } = {}) {
|
|
39
|
+
const skip = (page - 1) * limit;
|
|
40
|
+
const where = q ? { title: { contains: q, mode: 'insensitive' } } : {};
|
|
41
|
+
const orderBy = runOrderBy(sortBy, sortOrder);
|
|
42
|
+
const [runs, total] = await Promise.all([
|
|
43
|
+
prisma.testRun.findMany({ where, select: runListSelect, orderBy, skip, take: limit }),
|
|
44
|
+
prisma.testRun.count({ where })
|
|
45
|
+
]);
|
|
46
|
+
return { runs, total };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getById(id) {
|
|
50
|
+
return prisma.testRun.findUnique({
|
|
51
|
+
where: { id },
|
|
52
|
+
select: {
|
|
53
|
+
id: true,
|
|
54
|
+
title: true,
|
|
55
|
+
status: true,
|
|
56
|
+
createdAt: true,
|
|
57
|
+
updatedAt: true,
|
|
58
|
+
createdBy: { select: { id: true, name: true } },
|
|
59
|
+
entries: {
|
|
60
|
+
select: {
|
|
61
|
+
id: true,
|
|
62
|
+
order: true,
|
|
63
|
+
status: true,
|
|
64
|
+
notes: true,
|
|
65
|
+
executedAt: true,
|
|
66
|
+
executedBy: { select: { id: true, name: true } },
|
|
67
|
+
assignedToId: true,
|
|
68
|
+
assignedTo: { select: { id: true, name: true } },
|
|
69
|
+
case: {
|
|
70
|
+
select: {
|
|
71
|
+
id: true,
|
|
72
|
+
displayId: true,
|
|
73
|
+
title: true,
|
|
74
|
+
priority: true,
|
|
75
|
+
isAutomated: true,
|
|
76
|
+
suite: { select: { id: true, name: true, displayId: true } },
|
|
77
|
+
steps: { orderBy: { order: 'asc' } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
orderBy: { order: 'asc' }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function create({ title, caseIds, createdById }) {
|
|
88
|
+
const run = await prisma.testRun.create({
|
|
89
|
+
data: {
|
|
90
|
+
title,
|
|
91
|
+
status: 'backlog',
|
|
92
|
+
createdById,
|
|
93
|
+
entries: {
|
|
94
|
+
create: (caseIds ?? []).map((caseId, i) => ({ caseId, order: i }))
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
select: runListSelect
|
|
98
|
+
});
|
|
99
|
+
return run;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function update(id, { title, status, caseIds }) {
|
|
103
|
+
const data = {};
|
|
104
|
+
if (title !== undefined) data.title = title;
|
|
105
|
+
if (status !== undefined) data.status = status;
|
|
106
|
+
|
|
107
|
+
if (caseIds !== undefined) {
|
|
108
|
+
await prisma.testRunEntry.deleteMany({ where: { runId: id } });
|
|
109
|
+
await prisma.$transaction(
|
|
110
|
+
caseIds.map((caseId, i) =>
|
|
111
|
+
prisma.testRunEntry.create({ data: { runId: id, caseId, order: i } })
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return prisma.testRun.update({ where: { id }, data, select: runListSelect });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function duplicate(id, { createdById }) {
|
|
120
|
+
const original = await prisma.testRun.findUnique({
|
|
121
|
+
where: { id },
|
|
122
|
+
select: {
|
|
123
|
+
title: true,
|
|
124
|
+
entries: { select: { caseId: true, order: true }, orderBy: { order: 'asc' } }
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (!original) return null;
|
|
128
|
+
return prisma.testRun.create({
|
|
129
|
+
data: {
|
|
130
|
+
title: `Copy of ${original.title}`,
|
|
131
|
+
createdById,
|
|
132
|
+
entries: { create: original.entries.map((e, i) => ({ caseId: e.caseId, order: i })) }
|
|
133
|
+
},
|
|
134
|
+
select: runListSelect
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function remove(id) {
|
|
139
|
+
return prisma.testRun.delete({ where: { id } });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function updateEntry(entryId, { status, notes, executedById }) {
|
|
143
|
+
const entry = await prisma.testRunEntry.update({
|
|
144
|
+
where: { id: entryId },
|
|
145
|
+
data: {
|
|
146
|
+
status,
|
|
147
|
+
notes: notes ?? '',
|
|
148
|
+
executedById: executedById ?? null,
|
|
149
|
+
executedAt: new Date()
|
|
150
|
+
},
|
|
151
|
+
select: {
|
|
152
|
+
id: true,
|
|
153
|
+
status: true,
|
|
154
|
+
notes: true,
|
|
155
|
+
executedAt: true,
|
|
156
|
+
runId: true,
|
|
157
|
+
caseId: true
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (status === 'pass' || status === 'fail' || status === 'blocked' || status === 'skip') {
|
|
162
|
+
await prisma.testCaseHistory.create({
|
|
163
|
+
data: {
|
|
164
|
+
caseId: entry.caseId,
|
|
165
|
+
runId: entry.runId,
|
|
166
|
+
result: status,
|
|
167
|
+
source: 'manual',
|
|
168
|
+
notes: notes ?? '',
|
|
169
|
+
executedById: executedById ?? null
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return entry;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function assignEntry(entryId, { userId }) {
|
|
178
|
+
return prisma.testRunEntry.update({
|
|
179
|
+
where: { id: entryId },
|
|
180
|
+
data: { assignedToId: userId ?? null },
|
|
181
|
+
select: { id: true, assignedToId: true, assignedTo: { select: { id: true, name: true } } }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function reorderEntries(runId, orderedEntryIds) {
|
|
186
|
+
await prisma.$transaction(
|
|
187
|
+
orderedEntryIds.map((entryId, i) =>
|
|
188
|
+
prisma.testRunEntry.update({ where: { id: entryId }, data: { order: i } })
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
getAll,
|
|
195
|
+
getById,
|
|
196
|
+
create,
|
|
197
|
+
update,
|
|
198
|
+
duplicate,
|
|
199
|
+
remove,
|
|
200
|
+
updateEntry,
|
|
201
|
+
assignEntry,
|
|
202
|
+
reorderEntries
|
|
203
|
+
};
|