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.
- 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/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/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
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
A ready-to-use E2E test automation environment built on <a href="https://playwright.dev">Playwright</a> + <a href="https://cucumber.io">Cucumber</a>.<br/>
|
|
11
|
-
Write tests in Gherkin, run them from the CLI or UI,
|
|
11
|
+
Write tests in Gherkin, run them from the CLI or UI, view reports, and manage your entire test case repository — all in one place.
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
---
|
|
@@ -113,7 +113,23 @@ The first time you run it, Plum asks a few questions (press Enter to accept the
|
|
|
113
113
|
| **Frontend (UI) port** | `5173` | Host port for the web UI |
|
|
114
114
|
| **Primary public URL** | `http://<your-ip>:<port>` | The address you give runner nodes (see [Runner Setup](#4-runner-setup)) |
|
|
115
115
|
|
|
116
|
-
Your answers are saved to `.plum-server.json`, so the next `plum server start` reuses them without asking.
|
|
116
|
+
Your answers are saved to `.plum-server.json`, so the next `plum server start` reuses them without asking.
|
|
117
|
+
|
|
118
|
+
### First-user setup
|
|
119
|
+
|
|
120
|
+
On the very first start, Plum detects that no user accounts exist and prompts you to create one:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
No users found — create your first account to get started.
|
|
124
|
+
✔ Your name … Jane Smith
|
|
125
|
+
✔ Email address … jane@example.com
|
|
126
|
+
✔ Password …
|
|
127
|
+
✓ Account created for jane@example.com. You can now log in.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
After that, open the UI at the frontend port it prints (default **http://localhost:5173**) and sign in. Additional users can be invited from **Settings → Account** or the **Settings → Users** page.
|
|
131
|
+
|
|
132
|
+
> If you skip the CLI prompt, visit `http://localhost:5173/setup` in your browser to create the first account.
|
|
117
133
|
|
|
118
134
|
> Docker must be running before you use this command. Plum builds and starts the backend, database, and UI automatically.
|
|
119
135
|
|
|
@@ -292,7 +308,49 @@ plum run-test --browser firefox # run in a specific browser
|
|
|
292
308
|
|
|
293
309
|
---
|
|
294
310
|
|
|
295
|
-
## 4.
|
|
311
|
+
## 4. Test Repository
|
|
312
|
+
|
|
313
|
+
Plum includes a built-in test case management system accessible from the **Test Repository** tab in the UI.
|
|
314
|
+
|
|
315
|
+
### Test Suites and Cases
|
|
316
|
+
|
|
317
|
+
Organise test cases into **suites**. Each suite and case gets an auto-assigned ID (e.g. `TS-001`, `TC-001`). The prefix is configurable in **Settings → Repository**.
|
|
318
|
+
|
|
319
|
+
Each test case has:
|
|
320
|
+
|
|
321
|
+
- **Title** and **Description**
|
|
322
|
+
- **Priority** — Critical, High, Medium, or Low
|
|
323
|
+
- **Test Steps** — an ordered table with columns: Action, Test Data, Expected Output
|
|
324
|
+
- **Automated tag** — a Cucumber `@tag` name that links this case to an automated scenario (e.g. `test-login-1`)
|
|
325
|
+
- **History** — a timeline of every result, from manual test runs and automated builds
|
|
326
|
+
|
|
327
|
+
### Linking automated tests
|
|
328
|
+
|
|
329
|
+
Set the **Automated tag** on a test case to match a Cucumber `@tag` in your feature files. After every automated run, Plum scans the results and:
|
|
330
|
+
|
|
331
|
+
1. Marks matching cases as **automated**
|
|
332
|
+
2. Records a pass/fail entry in the case's history
|
|
333
|
+
|
|
334
|
+
```gherkin
|
|
335
|
+
@test-login-1
|
|
336
|
+
Scenario: User can log in with valid credentials
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
If `TC-042` has `automatedTag = test-login-1`, it will be marked automated and updated after each build — no manual linking required.
|
|
340
|
+
|
|
341
|
+
### Test Runs
|
|
342
|
+
|
|
343
|
+
Create a **Test Run** (e.g. "Sprint 12 regression") and drag test cases from the suite browser into the run. Then switch to **Execute** mode to step through each case and mark it pass / fail / blocked / skip.
|
|
344
|
+
|
|
345
|
+
Results are recorded in the case's history and the run's progress bar updates in real time.
|
|
346
|
+
|
|
347
|
+
### Migrating IDs
|
|
348
|
+
|
|
349
|
+
If you change the test ID prefix (e.g. from `TC` to `CASE`), use **Settings → Repository → Run Migration** to rename all existing IDs at once. Cucumber tags in your code are intentionally left unchanged — update those separately.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 5. Runner Setup
|
|
296
354
|
|
|
297
355
|
Runners are additional machines that execute tests in parallel alongside the primary server, letting you distribute a large suite across many nodes. A node runs as a plain Node process — **no Docker required**.
|
|
298
356
|
|
package/backend/app.js
CHANGED
|
@@ -39,6 +39,11 @@ if (process.env.PLUM_MODE !== 'node') {
|
|
|
39
39
|
app.use('/settings', require('./routes/settings.routes'));
|
|
40
40
|
app.use('/backup', require('./routes/backup.routes'));
|
|
41
41
|
app.use('/runners', require('./routes/runners.routes'));
|
|
42
|
+
app.use('/auth', require('./routes/auth.routes'));
|
|
43
|
+
app.use('/users', require('./routes/users.routes'));
|
|
44
|
+
app.use('/test-suites', require('./routes/test-suites.routes'));
|
|
45
|
+
app.use('/test-cases', require('./routes/test-cases.routes'));
|
|
46
|
+
app.use('/test-runs', require('./routes/test-runs.routes'));
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
module.exports = app;
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
import * as clack from '@clack/prompts';
|
|
19
|
+
import pc from 'picocolors';
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
const testsRoot = process.env.TESTS_ROOT || path.join(process.cwd(), 'tests');
|
|
24
|
+
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
/* Helpers */
|
|
27
|
+
/* ------------------------------------------------------------------ */
|
|
28
|
+
|
|
29
|
+
function toPascalCase(str) {
|
|
30
|
+
return str
|
|
31
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
32
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toKebabCase(str) {
|
|
36
|
+
return str
|
|
37
|
+
.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`)
|
|
38
|
+
.replace(/[-_\s]+/g, '-')
|
|
39
|
+
.replace(/^-/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Strip a trailing "Page" suffix so "CheckoutPage" → base "Checkout",
|
|
43
|
+
// then we append "Page" ourselves — avoiding "CheckoutPagePage".
|
|
44
|
+
function stripPageSuffix(pascal) {
|
|
45
|
+
return pascal.endsWith('Page') ? pascal.slice(0, -4) : pascal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* File generators */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
|
|
52
|
+
function generateFeature(pascal, kebab, suiteTag, testTag) {
|
|
53
|
+
return `${suiteTag}
|
|
54
|
+
Feature: ${pascal}
|
|
55
|
+
|
|
56
|
+
${testTag}
|
|
57
|
+
Scenario: Example scenario
|
|
58
|
+
Given I am on the ${pascal} page
|
|
59
|
+
When I perform an action
|
|
60
|
+
Then I should see the expected result
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function generatePage(base) {
|
|
65
|
+
return `import { page } from '../utils/browser';
|
|
66
|
+
|
|
67
|
+
export class ${base}Page {
|
|
68
|
+
static async goTo() {
|
|
69
|
+
await page().goto(process.env.BASE_URL as string);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static async performAction() {
|
|
73
|
+
// TODO: implement
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async verifyResult() {
|
|
77
|
+
// TODO: implement
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function generateSteps(pascal, base, kebab) {
|
|
84
|
+
return `import { Given, When, Then } from '@cucumber/cucumber';
|
|
85
|
+
import { ${base}Page } from '../pages/${base}Page';
|
|
86
|
+
|
|
87
|
+
Given('I am on the ${pascal} page', async () => {
|
|
88
|
+
await ${base}Page.goTo();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
When('I perform an action', async () => {
|
|
92
|
+
await ${base}Page.performAction();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Then('I should see the expected result', async () => {
|
|
96
|
+
await ${base}Page.verifyResult();
|
|
97
|
+
});
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ------------------------------------------------------------------ */
|
|
102
|
+
/* Main */
|
|
103
|
+
/* ------------------------------------------------------------------ */
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
clack.intro(pc.bgMagenta(pc.white(' 🟣 Plum — Create Test ')));
|
|
107
|
+
|
|
108
|
+
const rawName = await clack.text({
|
|
109
|
+
message: 'Feature name',
|
|
110
|
+
placeholder: 'Checkout, LoginPage, Cart…',
|
|
111
|
+
hint: 'Creates a .feature, Steps.ts and Page.ts',
|
|
112
|
+
validate: (v) => (!v.trim() ? 'Feature name is required' : undefined)
|
|
113
|
+
});
|
|
114
|
+
if (clack.isCancel(rawName)) {
|
|
115
|
+
clack.cancel('Cancelled.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const pascal = toPascalCase(rawName.trim());
|
|
120
|
+
const base = stripPageSuffix(pascal); // "CheckoutPage" → "Checkout", "Cart" → "Cart"
|
|
121
|
+
const kebab = toKebabCase(pascal);
|
|
122
|
+
const suiteTag = `@suite-${kebab}`;
|
|
123
|
+
const testTag = `@test-${kebab}-1`;
|
|
124
|
+
|
|
125
|
+
const featurePath = path.join(testsRoot, 'features', `${pascal}.feature`);
|
|
126
|
+
const pagePath = path.join(testsRoot, 'pages', `${base}Page.ts`);
|
|
127
|
+
const stepsPath = path.join(testsRoot, 'step_definitions', `${pascal}Steps.ts`);
|
|
128
|
+
|
|
129
|
+
const conflicts = [featurePath, pagePath, stepsPath].filter(fs.existsSync);
|
|
130
|
+
if (conflicts.length > 0) {
|
|
131
|
+
clack.log.error('The following files already exist:');
|
|
132
|
+
conflicts.forEach((f) => clack.log.warn(` ${path.relative(process.cwd(), f)}`));
|
|
133
|
+
clack.cancel('Delete them first or choose a different name.');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const s = clack.spinner();
|
|
138
|
+
s.start('Generating files…');
|
|
139
|
+
|
|
140
|
+
fs.mkdirSync(path.join(testsRoot, 'features'), { recursive: true });
|
|
141
|
+
fs.mkdirSync(path.join(testsRoot, 'pages'), { recursive: true });
|
|
142
|
+
fs.mkdirSync(path.join(testsRoot, 'step_definitions'), { recursive: true });
|
|
143
|
+
|
|
144
|
+
fs.writeFileSync(featurePath, generateFeature(pascal, kebab, suiteTag, testTag), 'utf8');
|
|
145
|
+
fs.writeFileSync(pagePath, generatePage(base), 'utf8');
|
|
146
|
+
fs.writeFileSync(stepsPath, generateSteps(pascal, base, kebab), 'utf8');
|
|
147
|
+
|
|
148
|
+
s.stop(pc.green('✓ Files created'));
|
|
149
|
+
|
|
150
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
151
|
+
|
|
152
|
+
clack.note(
|
|
153
|
+
[
|
|
154
|
+
`${pc.dim('Feature:')} ${pc.white(rel(featurePath))}`,
|
|
155
|
+
`${pc.dim('Page:')} ${pc.white(rel(pagePath))}`,
|
|
156
|
+
`${pc.dim('Steps:')} ${pc.white(rel(stepsPath))}`,
|
|
157
|
+
'',
|
|
158
|
+
`${pc.dim('Suite tag:')} ${pc.cyan(suiteTag)}`,
|
|
159
|
+
`${pc.dim('Test tag:')} ${pc.cyan(testTag)}`,
|
|
160
|
+
'',
|
|
161
|
+
`${pc.bold('Run with:')} ${pc.cyan(`plum run-test ${testTag}`)}`
|
|
162
|
+
].join('\n'),
|
|
163
|
+
`${pascal} scaffold`
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
clack.outro(pc.magenta('Done! Fill in your scenarios and implement the page methods.'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main().catch((err) => {
|
|
170
|
+
clack.log.error(err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
@@ -50,7 +50,8 @@ if (process.env.PLUM_MODE === 'node') process.exit(0);
|
|
|
50
50
|
nodeCount,
|
|
51
51
|
browser: process.env.BROWSER || 'chromium',
|
|
52
52
|
runnerName: process.env.RUNNER_NAME || null,
|
|
53
|
-
runnerId: process.env.RUNNER_ID || null
|
|
53
|
+
runnerId: process.env.RUNNER_ID || null,
|
|
54
|
+
testRunId: process.env.TEST_RUN_ID || null
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
console.log(`Report saved to database (id: ${saved.id})`);
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { verifyToken } = require('../services/userService');
|
|
19
|
+
|
|
20
|
+
function jwtAuth(req, res, next) {
|
|
21
|
+
const auth = req.headers.authorization;
|
|
22
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
23
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
req.user = verifyToken(auth.slice(7));
|
|
27
|
+
next();
|
|
28
|
+
} catch {
|
|
29
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { jwtAuth };
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
function requireAdmin(req, res, next) {
|
|
19
|
+
if (req.user?.role !== 'admin') {
|
|
20
|
+
return res.status(403).json({ error: 'Admin access required' });
|
|
21
|
+
}
|
|
22
|
+
next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { requireAdmin };
|
package/backend/package.json
CHANGED
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"@clack/prompts": "^1.5.1",
|
|
26
26
|
"@cucumber/cucumber": "^11.2.0",
|
|
27
27
|
"@prisma/client": "^6.19.3",
|
|
28
|
+
"bcryptjs": "^2.4.3",
|
|
28
29
|
"chai": "^4.3.6",
|
|
29
30
|
"chai-soft-assert": "^0.0.5",
|
|
30
31
|
"chokidar": "^5.0.0",
|
|
31
32
|
"cors": "^2.8.5",
|
|
32
33
|
"dotenv": "^16.4.7",
|
|
33
34
|
"express": "^4.21.2",
|
|
35
|
+
"jsonwebtoken": "^9.0.2",
|
|
34
36
|
"node-cron": "^3.0.3",
|
|
35
37
|
"picocolors": "^1.1.1",
|
|
36
38
|
"playwright": "^1.50.1",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- AlterTable Project: add test repository settings and sequence counters
|
|
2
|
+
ALTER TABLE "Project" ADD COLUMN "testCasePrefix" TEXT NOT NULL DEFAULT 'TC';
|
|
3
|
+
ALTER TABLE "Project" ADD COLUMN "testSuitePrefix" TEXT NOT NULL DEFAULT 'TS';
|
|
4
|
+
ALTER TABLE "Project" ADD COLUMN "caseSeqNext" INTEGER NOT NULL DEFAULT 0;
|
|
5
|
+
ALTER TABLE "Project" ADD COLUMN "suiteSeqNext" INTEGER NOT NULL DEFAULT 0;
|
|
6
|
+
|
|
7
|
+
-- AlterTable Report: add relation to TestCaseHistory (no schema change needed here, FK is on history side)
|
|
8
|
+
|
|
9
|
+
-- CreateTable User
|
|
10
|
+
CREATE TABLE "User" (
|
|
11
|
+
"id" TEXT NOT NULL,
|
|
12
|
+
"name" TEXT NOT NULL,
|
|
13
|
+
"email" TEXT NOT NULL,
|
|
14
|
+
"password" TEXT NOT NULL,
|
|
15
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
17
|
+
|
|
18
|
+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- CreateTable TestSuite
|
|
22
|
+
CREATE TABLE "TestSuite" (
|
|
23
|
+
"id" TEXT NOT NULL,
|
|
24
|
+
"displayId" TEXT NOT NULL,
|
|
25
|
+
"name" TEXT NOT NULL,
|
|
26
|
+
"description" TEXT NOT NULL DEFAULT '',
|
|
27
|
+
"priority" TEXT NOT NULL DEFAULT 'Medium',
|
|
28
|
+
"createdById" TEXT NOT NULL,
|
|
29
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
31
|
+
|
|
32
|
+
CONSTRAINT "TestSuite_pkey" PRIMARY KEY ("id")
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- CreateTable TestCase
|
|
36
|
+
CREATE TABLE "TestCase" (
|
|
37
|
+
"id" TEXT NOT NULL,
|
|
38
|
+
"displayId" TEXT NOT NULL,
|
|
39
|
+
"title" TEXT NOT NULL,
|
|
40
|
+
"description" TEXT NOT NULL DEFAULT '',
|
|
41
|
+
"priority" TEXT NOT NULL DEFAULT 'Medium',
|
|
42
|
+
"automatedTag" TEXT,
|
|
43
|
+
"isAutomated" BOOLEAN NOT NULL DEFAULT false,
|
|
44
|
+
"suiteId" TEXT NOT NULL,
|
|
45
|
+
"createdById" TEXT NOT NULL,
|
|
46
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
48
|
+
|
|
49
|
+
CONSTRAINT "TestCase_pkey" PRIMARY KEY ("id")
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- CreateTable TestStep
|
|
53
|
+
CREATE TABLE "TestStep" (
|
|
54
|
+
"id" TEXT NOT NULL,
|
|
55
|
+
"caseId" TEXT NOT NULL,
|
|
56
|
+
"action" TEXT NOT NULL,
|
|
57
|
+
"testData" TEXT NOT NULL DEFAULT '',
|
|
58
|
+
"expectedOutput" TEXT NOT NULL DEFAULT '',
|
|
59
|
+
"order" INTEGER NOT NULL,
|
|
60
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
|
|
62
|
+
CONSTRAINT "TestStep_pkey" PRIMARY KEY ("id")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- CreateTable TestRun
|
|
66
|
+
CREATE TABLE "TestRun" (
|
|
67
|
+
"id" TEXT NOT NULL,
|
|
68
|
+
"title" TEXT NOT NULL,
|
|
69
|
+
"status" TEXT NOT NULL DEFAULT 'draft',
|
|
70
|
+
"createdById" TEXT NOT NULL,
|
|
71
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
72
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
73
|
+
|
|
74
|
+
CONSTRAINT "TestRun_pkey" PRIMARY KEY ("id")
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
-- CreateTable TestRunEntry
|
|
78
|
+
CREATE TABLE "TestRunEntry" (
|
|
79
|
+
"id" TEXT NOT NULL,
|
|
80
|
+
"runId" TEXT NOT NULL,
|
|
81
|
+
"caseId" TEXT NOT NULL,
|
|
82
|
+
"status" TEXT NOT NULL DEFAULT 'pending',
|
|
83
|
+
"notes" TEXT NOT NULL DEFAULT '',
|
|
84
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
"executedById" TEXT,
|
|
86
|
+
"executedAt" TIMESTAMP(3),
|
|
87
|
+
|
|
88
|
+
CONSTRAINT "TestRunEntry_pkey" PRIMARY KEY ("id")
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
-- CreateTable TestCaseHistory
|
|
92
|
+
CREATE TABLE "TestCaseHistory" (
|
|
93
|
+
"id" TEXT NOT NULL,
|
|
94
|
+
"caseId" TEXT NOT NULL,
|
|
95
|
+
"runId" TEXT,
|
|
96
|
+
"reportId" INTEGER,
|
|
97
|
+
"result" TEXT NOT NULL,
|
|
98
|
+
"source" TEXT NOT NULL DEFAULT 'manual',
|
|
99
|
+
"notes" TEXT NOT NULL DEFAULT '',
|
|
100
|
+
"executedById" TEXT,
|
|
101
|
+
"executedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
102
|
+
|
|
103
|
+
CONSTRAINT "TestCaseHistory_pkey" PRIMARY KEY ("id")
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
-- CreateIndex
|
|
107
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
108
|
+
CREATE UNIQUE INDEX "TestSuite_displayId_key" ON "TestSuite"("displayId");
|
|
109
|
+
CREATE UNIQUE INDEX "TestCase_displayId_key" ON "TestCase"("displayId");
|
|
110
|
+
|
|
111
|
+
-- AddForeignKey
|
|
112
|
+
ALTER TABLE "TestSuite" ADD CONSTRAINT "TestSuite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
113
|
+
|
|
114
|
+
-- AddForeignKey
|
|
115
|
+
ALTER TABLE "TestCase" ADD CONSTRAINT "TestCase_suiteId_fkey" FOREIGN KEY ("suiteId") REFERENCES "TestSuite"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
116
|
+
ALTER TABLE "TestCase" ADD CONSTRAINT "TestCase_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
117
|
+
|
|
118
|
+
-- AddForeignKey
|
|
119
|
+
ALTER TABLE "TestStep" ADD CONSTRAINT "TestStep_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
120
|
+
|
|
121
|
+
-- AddForeignKey
|
|
122
|
+
ALTER TABLE "TestRun" ADD CONSTRAINT "TestRun_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
123
|
+
|
|
124
|
+
-- AddForeignKey
|
|
125
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_runId_fkey" FOREIGN KEY ("runId") REFERENCES "TestRun"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
126
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
127
|
+
ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_executedById_fkey" FOREIGN KEY ("executedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
128
|
+
|
|
129
|
+
-- AddForeignKey
|
|
130
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "TestCase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
131
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_runId_fkey" FOREIGN KEY ("runId") REFERENCES "TestRun"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
132
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "Report"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
133
|
+
ALTER TABLE "TestCaseHistory" ADD CONSTRAINT "TestCaseHistory_executedById_fkey" FOREIGN KEY ("executedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -51,23 +51,131 @@ model CronJob {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
model Report {
|
|
54
|
-
id Int
|
|
54
|
+
id Int @id @default(autoincrement())
|
|
55
55
|
status String
|
|
56
56
|
tags String
|
|
57
|
-
triggerType String
|
|
58
|
-
runners Int
|
|
59
|
-
browser String
|
|
57
|
+
triggerType String @default("manual-trigger")
|
|
58
|
+
runners Int @default(1)
|
|
59
|
+
browser String @default("chromium")
|
|
60
60
|
runnerName String?
|
|
61
61
|
runnerId String?
|
|
62
|
-
runner Runner?
|
|
62
|
+
runner Runner? @relation(fields: [runnerId], references: [id], onDelete: SetNull)
|
|
63
63
|
cronJobId Int?
|
|
64
|
-
cronJob CronJob?
|
|
64
|
+
cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
|
|
65
65
|
content Json
|
|
66
|
-
createdAt DateTime
|
|
66
|
+
createdAt DateTime @default(now())
|
|
67
|
+
testHistory TestCaseHistory[]
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
model Project {
|
|
70
|
-
id
|
|
71
|
-
name
|
|
72
|
-
logoUrl
|
|
71
|
+
id Int @id @default(autoincrement())
|
|
72
|
+
name String @default("")
|
|
73
|
+
logoUrl String @default("")
|
|
74
|
+
testCasePrefix String @default("TC")
|
|
75
|
+
testSuitePrefix String @default("TS")
|
|
76
|
+
caseSeqNext Int @default(0)
|
|
77
|
+
suiteSeqNext Int @default(0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
model User {
|
|
81
|
+
id String @id @default(cuid())
|
|
82
|
+
name String
|
|
83
|
+
email String @unique
|
|
84
|
+
password String
|
|
85
|
+
role String @default("user")
|
|
86
|
+
createdAt DateTime @default(now())
|
|
87
|
+
updatedAt DateTime @updatedAt
|
|
88
|
+
|
|
89
|
+
suites TestSuite[]
|
|
90
|
+
cases TestCase[]
|
|
91
|
+
runs TestRun[]
|
|
92
|
+
executedEntries TestRunEntry[] @relation("entryExecutor")
|
|
93
|
+
assignedEntries TestRunEntry[] @relation("entryAssignee")
|
|
94
|
+
caseHistories TestCaseHistory[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
model TestSuite {
|
|
98
|
+
id String @id @default(cuid())
|
|
99
|
+
displayId String @unique
|
|
100
|
+
name String
|
|
101
|
+
description String @default("")
|
|
102
|
+
priority String @default("Medium")
|
|
103
|
+
createdById String
|
|
104
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
105
|
+
createdAt DateTime @default(now())
|
|
106
|
+
updatedAt DateTime @updatedAt
|
|
107
|
+
cases TestCase[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
model TestCase {
|
|
111
|
+
id String @id @default(cuid())
|
|
112
|
+
displayId String @unique
|
|
113
|
+
title String
|
|
114
|
+
description String @default("")
|
|
115
|
+
priority String @default("Medium")
|
|
116
|
+
isAutomated Boolean @default(false)
|
|
117
|
+
suiteId String
|
|
118
|
+
suite TestSuite @relation(fields: [suiteId], references: [id], onDelete: Cascade)
|
|
119
|
+
createdById String
|
|
120
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
121
|
+
createdAt DateTime @default(now())
|
|
122
|
+
updatedAt DateTime @updatedAt
|
|
123
|
+
steps TestStep[]
|
|
124
|
+
runEntries TestRunEntry[]
|
|
125
|
+
history TestCaseHistory[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
model TestStep {
|
|
129
|
+
id String @id @default(cuid())
|
|
130
|
+
caseId String
|
|
131
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
132
|
+
action String
|
|
133
|
+
testData String @default("")
|
|
134
|
+
expectedOutput String @default("")
|
|
135
|
+
order Int
|
|
136
|
+
createdAt DateTime @default(now())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
model TestRun {
|
|
140
|
+
id String @id @default(cuid())
|
|
141
|
+
title String
|
|
142
|
+
status String @default("backlog")
|
|
143
|
+
createdById String
|
|
144
|
+
createdBy User @relation(fields: [createdById], references: [id])
|
|
145
|
+
createdAt DateTime @default(now())
|
|
146
|
+
updatedAt DateTime @updatedAt
|
|
147
|
+
entries TestRunEntry[]
|
|
148
|
+
history TestCaseHistory[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
model TestRunEntry {
|
|
152
|
+
id String @id @default(cuid())
|
|
153
|
+
runId String
|
|
154
|
+
run TestRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
155
|
+
caseId String
|
|
156
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
157
|
+
status String @default("pending")
|
|
158
|
+
notes String @default("")
|
|
159
|
+
order Int @default(0)
|
|
160
|
+
executedById String?
|
|
161
|
+
executedBy User? @relation("entryExecutor", fields: [executedById], references: [id])
|
|
162
|
+
executedAt DateTime?
|
|
163
|
+
assignedToId String?
|
|
164
|
+
assignedTo User? @relation("entryAssignee", fields: [assignedToId], references: [id])
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
model TestCaseHistory {
|
|
168
|
+
id String @id @default(cuid())
|
|
169
|
+
caseId String
|
|
170
|
+
case TestCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
|
|
171
|
+
runId String?
|
|
172
|
+
run TestRun? @relation(fields: [runId], references: [id], onDelete: SetNull)
|
|
173
|
+
reportId Int?
|
|
174
|
+
report Report? @relation(fields: [reportId], references: [id], onDelete: SetNull)
|
|
175
|
+
result String
|
|
176
|
+
source String @default("manual")
|
|
177
|
+
notes String @default("")
|
|
178
|
+
executedById String?
|
|
179
|
+
executedBy User? @relation(fields: [executedById], references: [id])
|
|
180
|
+
executedAt DateTime @default(now())
|
|
73
181
|
}
|