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
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, and view reports — all in one place.
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. When it finishes, open the UI at the frontend port it prints (default **http://localhost:5173**).
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. Runner Setup
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 };
@@ -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;
@@ -0,0 +1,3 @@
1
+ -- AlterTable User: add role column, promote first user to admin
2
+ ALTER TABLE "User" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'user';
3
+ UPDATE "User" SET "role" = 'admin' WHERE "id" = (SELECT "id" FROM "User" ORDER BY "createdAt" ASC LIMIT 1);
@@ -0,0 +1,2 @@
1
+ -- TestCase.automatedTag removed: displayId is now used directly as the Cucumber tag identifier
2
+ ALTER TABLE "TestCase" DROP COLUMN IF EXISTS "automatedTag";
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "TestRunEntry" ADD COLUMN "assignedToId" TEXT;
2
+ ALTER TABLE "TestRunEntry" ADD CONSTRAINT "TestRunEntry_assignedToId_fkey" FOREIGN KEY ("assignedToId") 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 @id @default(autoincrement())
54
+ id Int @id @default(autoincrement())
55
55
  status String
56
56
  tags String
57
- triggerType String @default("manual-trigger")
58
- runners Int @default(1)
59
- browser String @default("chromium")
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? @relation(fields: [runnerId], references: [id], onDelete: SetNull)
62
+ runner Runner? @relation(fields: [runnerId], references: [id], onDelete: SetNull)
63
63
  cronJobId Int?
64
- cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
64
+ cronJob CronJob? @relation(fields: [cronJobId], references: [id], onDelete: SetNull)
65
65
  content Json
66
- createdAt DateTime @default(now())
66
+ createdAt DateTime @default(now())
67
+ testHistory TestCaseHistory[]
67
68
  }
68
69
 
69
70
  model Project {
70
- id Int @id @default(autoincrement())
71
- name String @default("")
72
- logoUrl String @default("")
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
  }