plum-e2e 2.4.13 → 2.5.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/backend/_scaffold/utils/browser.ts +47 -2
- package/backend/_scaffold/utils/hooks.ts +25 -2
- package/backend/config/scripts/run-tests.js +35 -13
- package/backend/logs/runner-cmr8o2x3n0000mg01410tpuck.log +20 -0
- package/backend/logs/runner-cmr90hvcx0000n2016edtmrtv.log +43 -0
- package/backend/logs/runner-cmr90i7h20001n201g2wg6v3n.log +43 -0
- package/backend/logs/runner-cmr90iilm0002n201mmisva95.log +43 -0
- package/backend/logs/runner-cmr90j70r0003n201xz8525dz.log +20 -0
- package/backend/logs/runner-cmr90yup70000qp017ianc2gp.log +43 -0
- package/backend/logs/runner-cmr90zbtm0001qp01u9tfdkh1.log +20 -0
- package/backend/logs/runner-cmr91a3mw0000tb01414rd1df.log +20 -0
- package/backend/logs/runner-cmr91dhla0001tb01jp7thjnl.log +20 -0
- package/backend/prisma/migrations/20260706000000_add_report_logs/migration.sql +2 -0
- package/backend/prisma/schema.prisma +1 -0
- package/backend/routes/node.routes.js +42 -4
- package/backend/services/reportService.js +24 -4
- package/backend/services/runnerService.js +11 -1
- package/backend/websockets/socketHandler.js +162 -16
- package/frontend/package.json +1 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +53 -55
- package/frontend/src/lib/components/ui/AutomatedBadge.svelte +56 -0
- package/frontend/src/lib/components/ui/BackLink.svelte +77 -0
- package/frontend/src/lib/components/ui/Badge.svelte +1 -1
- package/frontend/src/lib/components/ui/Button.svelte +1 -1
- package/frontend/src/lib/components/ui/CaseIdChip.svelte +59 -0
- package/frontend/src/lib/components/ui/ConfirmModal.svelte +1 -1
- package/frontend/src/lib/components/ui/Pagination.svelte +1 -1
- package/frontend/src/lib/components/ui/PriorityBadge.svelte +83 -0
- package/frontend/src/lib/components/ui/ResultChip.svelte +92 -0
- package/frontend/src/lib/components/ui/StatusDot.svelte +64 -0
- package/frontend/src/lib/components/ui/StepKeyword.svelte +79 -0
- package/frontend/src/lib/components/ui/StepStatusIcon.svelte +66 -0
- package/frontend/src/lib/components/ui/TagChip.svelte +51 -0
- package/frontend/src/lib/constants.js +14 -0
- package/frontend/src/lib/stores/runner.js +5 -3
- package/frontend/src/lib/styles/tokens.css +26 -0
- package/frontend/src/lib/utils/format.js +74 -0
- package/frontend/src/routes/+page.svelte +3 -3
- package/frontend/src/routes/login/+page.svelte +1 -1
- package/frontend/src/routes/reports/+page.svelte +3 -3
- package/frontend/src/routes/reports/[id]/+page.svelte +703 -215
- package/frontend/src/routes/reports/live/+page.svelte +281 -283
- package/frontend/src/routes/scheduled-tests/+page.svelte +3 -3
- package/frontend/src/routes/settings/+page.svelte +6 -6
- package/frontend/src/routes/setup/+page.svelte +1 -1
- package/frontend/src/routes/test-repository/+page.svelte +5 -5
- package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +27 -151
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +4 -4
- package/package.json +1 -1
|
@@ -1,8 +1,28 @@
|
|
|
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
|
+
|
|
1
18
|
import { chromium, firefox, webkit, Browser, BrowserContext, Page } from 'playwright';
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
2
21
|
|
|
3
22
|
let _browser: Browser;
|
|
4
23
|
let _context: BrowserContext;
|
|
5
24
|
let _page: Page;
|
|
25
|
+
let _ssCounter = 0;
|
|
6
26
|
|
|
7
27
|
export const page = (): Page => _page;
|
|
8
28
|
|
|
@@ -16,14 +36,39 @@ export async function setup(): Promise<void> {
|
|
|
16
36
|
_page = await _context.newPage();
|
|
17
37
|
}
|
|
18
38
|
|
|
39
|
+
export async function screenshotStep(
|
|
40
|
+
attach: (data: Buffer, mime: string) => Promise<void>
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (!_page) return;
|
|
43
|
+
try {
|
|
44
|
+
const screenshot = await _page.screenshot({ type: 'png' });
|
|
45
|
+
await attach(screenshot, 'image/png');
|
|
46
|
+
} catch {
|
|
47
|
+
// page may be navigating or already closed
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function streamLiveScreenshot(stepName: string): Promise<void> {
|
|
52
|
+
const ssDir = process.env.PLUM_SS_DIR;
|
|
53
|
+
if (!ssDir || !_page) return;
|
|
54
|
+
try {
|
|
55
|
+
const seq = `${String(Date.now()).padStart(16, '0')}-${String(++_ssCounter).padStart(4, '0')}`;
|
|
56
|
+
const screenshot = await _page.screenshot({ type: 'jpeg', quality: 70 });
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(ssDir, `${seq}.ss.json`),
|
|
59
|
+
JSON.stringify({ stepName, data: screenshot.toString('base64') })
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore — live streaming is best-effort
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
19
66
|
export async function teardown(
|
|
20
67
|
attach: (data: Buffer, mime: string) => Promise<void>,
|
|
21
68
|
failed: boolean
|
|
22
69
|
): Promise<void> {
|
|
23
70
|
if (failed && _page) {
|
|
24
71
|
const screenshotDir = 'reports/screenshots';
|
|
25
|
-
const fs = await import('fs');
|
|
26
|
-
const path = await import('path');
|
|
27
72
|
if (!fs.existsSync(screenshotDir)) {
|
|
28
73
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
29
74
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 { Before, After, AfterStep, ITestCaseHookParameter } from '@cucumber/cucumber';
|
|
19
|
+
import { setup, teardown, screenshotStep, streamLiveScreenshot } from './browser';
|
|
3
20
|
import dotenv from 'dotenv';
|
|
4
21
|
|
|
5
22
|
dotenv.config();
|
|
@@ -10,6 +27,12 @@ Before(async ({ pickle }: ITestCaseHookParameter) => {
|
|
|
10
27
|
await setup();
|
|
11
28
|
});
|
|
12
29
|
|
|
30
|
+
AfterStep(async function ({ pickleStep, result }: { pickleStep: any; result: any }) {
|
|
31
|
+
if (result?.status === 'SKIPPED') return;
|
|
32
|
+
await screenshotStep(this.attach.bind(this));
|
|
33
|
+
await streamLiveScreenshot(pickleStep?.text ?? '');
|
|
34
|
+
});
|
|
35
|
+
|
|
13
36
|
After(async function (scenario: ITestCaseHookParameter) {
|
|
14
37
|
await teardown(this.attach.bind(this), scenario.result?.status === 'FAILED');
|
|
15
38
|
});
|
|
@@ -52,21 +52,39 @@ try {
|
|
|
52
52
|
`BROWSER=${browser}`,
|
|
53
53
|
'TS_NODE_TRANSPILE_ONLY=true',
|
|
54
54
|
'cucumber-js',
|
|
55
|
-
|
|
55
|
+
// Pass the directory, not a glob — avoids shell globstar expansion differences
|
|
56
|
+
// across platforms and lets cucumber-js discover .feature files recursively.
|
|
57
|
+
`${testsRoot}/features`
|
|
56
58
|
];
|
|
57
59
|
|
|
58
|
-
// A dispatched run executes test files in a temp dir on a runner node.
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// skipped entirely (an explicit --config disables auto-discovery of the cwd file).
|
|
60
|
+
// A dispatched run executes test files in a temp dir on a runner node. Running
|
|
61
|
+
// cucumber from that temp dir (cwd = testsRootAbs) makes it auto-discover the
|
|
62
|
+
// cucumber.json written there, so the node's own backend cucumber.json is never
|
|
63
|
+
// loaded — preventing step-definition conflicts without relying on --config path
|
|
64
|
+
// resolution (which is fragile when the temp path is outside the project).
|
|
65
|
+
let execCwd;
|
|
65
66
|
if (process.env.TESTS_ROOT) {
|
|
66
|
-
const testsRootAbs = path.resolve(testsRoot);
|
|
67
|
-
|
|
67
|
+
const testsRootAbs = path.isAbsolute(testsRoot) ? testsRoot : path.resolve(testsRoot);
|
|
68
|
+
execCwd = testsRootAbs;
|
|
69
|
+
|
|
70
|
+
// hooks.ts calls dotenv.config() with no path, which defaults to process.cwd().
|
|
71
|
+
// When running from a temp dir there is no .env there, so vars like BASE_URL
|
|
72
|
+
// would be undefined. Pre-load BACKEND_DIR's .env so they are already in
|
|
73
|
+
// process.env before dotenv.config() runs (dotenv never overwrites existing vars).
|
|
74
|
+
const backendEnvPath = path.resolve(__dirname, '..', '..', '.env');
|
|
75
|
+
if (fs.existsSync(backendEnvPath)) {
|
|
76
|
+
for (const line of fs.readFileSync(backendEnvPath, 'utf8').split(/\r?\n/)) {
|
|
77
|
+
const m = line.match(/^([A-Za-z_]\w*)\s*=\s*(.*?)(\s*#.*)?$/);
|
|
78
|
+
if (m) {
|
|
79
|
+
const key = m[1];
|
|
80
|
+
const val = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
81
|
+
if (!(key in process.env)) process.env[key] = val;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
fs.writeFileSync(
|
|
69
|
-
|
|
87
|
+
path.join(testsRootAbs, 'cucumber.json'),
|
|
70
88
|
JSON.stringify({
|
|
71
89
|
default: {
|
|
72
90
|
requireModule: ['ts-node/register'],
|
|
@@ -77,7 +95,6 @@ try {
|
|
|
77
95
|
}
|
|
78
96
|
})
|
|
79
97
|
);
|
|
80
|
-
baseCommand.push('--config', path.relative(process.cwd(), configPath).replace(/\\/g, '/'));
|
|
81
98
|
} else {
|
|
82
99
|
baseCommand.push(
|
|
83
100
|
'--require-module',
|
|
@@ -102,11 +119,16 @@ try {
|
|
|
102
119
|
const cucumberCommand = baseCommand.join(' ');
|
|
103
120
|
execSync(cucumberCommand, {
|
|
104
121
|
stdio: 'inherit',
|
|
122
|
+
...(execCwd && { cwd: execCwd }),
|
|
105
123
|
env: {
|
|
106
124
|
...process.env,
|
|
107
125
|
NODE_PATH: process.env.NODE_PATH
|
|
108
126
|
? `${nodeModulesPath}${path.delimiter}${process.env.NODE_PATH}`
|
|
109
|
-
: nodeModulesPath
|
|
127
|
+
: nodeModulesPath,
|
|
128
|
+
// When running from a temp dir there is no tsconfig.json above it, so
|
|
129
|
+
// ts-node falls back to defaults that may conflict with the Node version.
|
|
130
|
+
// Point it at the backend tsconfig explicitly.
|
|
131
|
+
...(execCwd && { TS_NODE_PROJECT: path.resolve(__dirname, '..', '..', 'tsconfig.json') })
|
|
110
132
|
}
|
|
111
133
|
});
|
|
112
134
|
} catch (error) {
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 3002 (node/runner mode)
|
|
19
|
+
(node:89713) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
node:events:487
|
|
19
|
+
throw er; // Unhandled 'error' event
|
|
20
|
+
^
|
|
21
|
+
|
|
22
|
+
Error: listen EADDRINUSE: address already in use :::3002
|
|
23
|
+
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
|
|
24
|
+
at listenInCluster (node:net:2065:12)
|
|
25
|
+
at Server.listen (node:net:2170:7)
|
|
26
|
+
at start (/Users/silverlunah/Projects/plum/backend/server.js:65:9)
|
|
27
|
+
at Object.<anonymous> (/Users/silverlunah/Projects/plum/backend/server.js:139:1)
|
|
28
|
+
at Module._compile (node:internal/modules/cjs/loader:1829:14)
|
|
29
|
+
at Module._extensions..js (node:internal/modules/cjs/loader:1969:10)
|
|
30
|
+
at Module.load (node:internal/modules/cjs/loader:1552:32)
|
|
31
|
+
at Module._load (node:internal/modules/cjs/loader:1354:12)
|
|
32
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
|
|
33
|
+
Emitted 'error' event on Server instance at:
|
|
34
|
+
at emitErrorNT (node:net:2044:8)
|
|
35
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
|
|
36
|
+
code: 'EADDRINUSE',
|
|
37
|
+
errno: -48,
|
|
38
|
+
syscall: 'listen',
|
|
39
|
+
address: '::',
|
|
40
|
+
port: 3002
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Node.js v25.9.0
|
|
@@ -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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
node:events:487
|
|
19
|
+
throw er; // Unhandled 'error' event
|
|
20
|
+
^
|
|
21
|
+
|
|
22
|
+
Error: listen EADDRINUSE: address already in use :::3003
|
|
23
|
+
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
|
|
24
|
+
at listenInCluster (node:net:2065:12)
|
|
25
|
+
at Server.listen (node:net:2170:7)
|
|
26
|
+
at start (/Users/silverlunah/Projects/plum/backend/server.js:65:9)
|
|
27
|
+
at Object.<anonymous> (/Users/silverlunah/Projects/plum/backend/server.js:139:1)
|
|
28
|
+
at Module._compile (node:internal/modules/cjs/loader:1829:14)
|
|
29
|
+
at Module._extensions..js (node:internal/modules/cjs/loader:1969:10)
|
|
30
|
+
at Module.load (node:internal/modules/cjs/loader:1552:32)
|
|
31
|
+
at Module._load (node:internal/modules/cjs/loader:1354:12)
|
|
32
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
|
|
33
|
+
Emitted 'error' event on Server instance at:
|
|
34
|
+
at emitErrorNT (node:net:2044:8)
|
|
35
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
|
|
36
|
+
code: 'EADDRINUSE',
|
|
37
|
+
errno: -48,
|
|
38
|
+
syscall: 'listen',
|
|
39
|
+
address: '::',
|
|
40
|
+
port: 3003
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Node.js v25.9.0
|
|
@@ -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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
node:events:487
|
|
19
|
+
throw er; // Unhandled 'error' event
|
|
20
|
+
^
|
|
21
|
+
|
|
22
|
+
Error: listen EADDRINUSE: address already in use :::3009
|
|
23
|
+
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
|
|
24
|
+
at listenInCluster (node:net:2065:12)
|
|
25
|
+
at Server.listen (node:net:2170:7)
|
|
26
|
+
at start (/Users/silverlunah/Projects/plum/backend/server.js:65:9)
|
|
27
|
+
at Object.<anonymous> (/Users/silverlunah/Projects/plum/backend/server.js:139:1)
|
|
28
|
+
at Module._compile (node:internal/modules/cjs/loader:1829:14)
|
|
29
|
+
at Module._extensions..js (node:internal/modules/cjs/loader:1969:10)
|
|
30
|
+
at Module.load (node:internal/modules/cjs/loader:1552:32)
|
|
31
|
+
at Module._load (node:internal/modules/cjs/loader:1354:12)
|
|
32
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
|
|
33
|
+
Emitted 'error' event on Server instance at:
|
|
34
|
+
at emitErrorNT (node:net:2044:8)
|
|
35
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
|
|
36
|
+
code: 'EADDRINUSE',
|
|
37
|
+
errno: -48,
|
|
38
|
+
syscall: 'listen',
|
|
39
|
+
address: '::',
|
|
40
|
+
port: 3009
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Node.js v25.9.0
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 5177 (node/runner mode)
|
|
19
|
+
(node:98310) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
node:events:487
|
|
19
|
+
throw er; // Unhandled 'error' event
|
|
20
|
+
^
|
|
21
|
+
|
|
22
|
+
Error: listen EADDRINUSE: address already in use :::3002
|
|
23
|
+
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
|
|
24
|
+
at listenInCluster (node:net:2065:12)
|
|
25
|
+
at Server.listen (node:net:2170:7)
|
|
26
|
+
at start (/Users/silverlunah/Projects/plum/backend/server.js:65:9)
|
|
27
|
+
at Object.<anonymous> (/Users/silverlunah/Projects/plum/backend/server.js:139:1)
|
|
28
|
+
at Module._compile (node:internal/modules/cjs/loader:1829:14)
|
|
29
|
+
at Module._extensions..js (node:internal/modules/cjs/loader:1969:10)
|
|
30
|
+
at Module.load (node:internal/modules/cjs/loader:1552:32)
|
|
31
|
+
at Module._load (node:internal/modules/cjs/loader:1354:12)
|
|
32
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
|
|
33
|
+
Emitted 'error' event on Server instance at:
|
|
34
|
+
at emitErrorNT (node:net:2044:8)
|
|
35
|
+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
|
|
36
|
+
code: 'EADDRINUSE',
|
|
37
|
+
errno: -48,
|
|
38
|
+
syscall: 'listen',
|
|
39
|
+
address: '::',
|
|
40
|
+
port: 3002
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Node.js v25.9.0
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 5178 (node/runner mode)
|
|
19
|
+
(node:825) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 5178 (node/runner mode)
|
|
19
|
+
(node:2599) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
📂 Loading tests from: /Users/silverlunah/Projects/plum/backend/tests
|
|
18
|
+
Backend running on port 5180 (node/runner mode)
|
|
19
|
+
(node:3077) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
|
|
20
|
+
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
@@ -50,10 +50,13 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
50
50
|
const { tags, browser = 'chromium', workers = 1, tests = null } = req.body;
|
|
51
51
|
const jobId = crypto.randomUUID();
|
|
52
52
|
|
|
53
|
+
// path.resolve ensures absolute even if TMPDIR env var is set to a relative path
|
|
54
|
+
const tmpdir = path.resolve(os.tmpdir());
|
|
55
|
+
|
|
53
56
|
// Write test files sent by the primary into a per-job temp dir
|
|
54
57
|
let tempTestsDir = null;
|
|
55
58
|
if (tests && Object.keys(tests).length > 0) {
|
|
56
|
-
tempTestsDir = path.join(
|
|
59
|
+
tempTestsDir = path.join(tmpdir, `plum-job-${jobId}`);
|
|
57
60
|
for (const [rel, content] of Object.entries(tests)) {
|
|
58
61
|
const dest = path.join(tempTestsDir, rel);
|
|
59
62
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -63,7 +66,9 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
63
66
|
|
|
64
67
|
// Each job writes to its own temp file so concurrent jobs on the same node
|
|
65
68
|
// cannot clobber each other's reports (shared cucumber_report.json race condition).
|
|
66
|
-
const reportFile = path.join(
|
|
69
|
+
const reportFile = path.join(tmpdir, `plum-report-${jobId}.json`);
|
|
70
|
+
const ssDir = path.join(tmpdir, `plum-ss-${jobId}`);
|
|
71
|
+
fs.mkdirSync(ssDir, { recursive: true });
|
|
67
72
|
|
|
68
73
|
jobs[jobId] = {
|
|
69
74
|
status: 'running',
|
|
@@ -72,7 +77,9 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
72
77
|
startedAt: Date.now(),
|
|
73
78
|
meta: { tags: tags || '', browser, workers },
|
|
74
79
|
tempTestsDir,
|
|
75
|
-
reportFile
|
|
80
|
+
reportFile,
|
|
81
|
+
ssDir,
|
|
82
|
+
pendingScreenshots: []
|
|
76
83
|
};
|
|
77
84
|
|
|
78
85
|
const env = {
|
|
@@ -82,10 +89,36 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
82
89
|
BROWSER: browser,
|
|
83
90
|
REPORT_RUNNERS: String(workers),
|
|
84
91
|
CUCUMBER_REPORT_FILE: reportFile,
|
|
92
|
+
PLUM_SS_DIR: ssDir,
|
|
85
93
|
...(tempTestsDir ? { TESTS_ROOT: tempTestsDir } : {})
|
|
86
94
|
};
|
|
87
95
|
if (workers > 1) env.PARALLEL = String(workers);
|
|
88
96
|
|
|
97
|
+
const seenSsFiles = new Set();
|
|
98
|
+
const ssPoller = setInterval(() => {
|
|
99
|
+
const job = jobs[jobId];
|
|
100
|
+
if (!job) {
|
|
101
|
+
clearInterval(ssPoller);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const files = fs
|
|
106
|
+
.readdirSync(ssDir)
|
|
107
|
+
.filter((f) => f.endsWith('.ss.json'))
|
|
108
|
+
.sort();
|
|
109
|
+
for (const f of files) {
|
|
110
|
+
if (seenSsFiles.has(f)) continue;
|
|
111
|
+
seenSsFiles.add(f);
|
|
112
|
+
const filePath = path.join(ssDir, f);
|
|
113
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
114
|
+
job.pendingScreenshots.push(data);
|
|
115
|
+
try {
|
|
116
|
+
fs.unlinkSync(filePath);
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
} catch {}
|
|
120
|
+
}, 400);
|
|
121
|
+
|
|
89
122
|
const proc = spawn('npm', ['run', 'test'], { env, shell: true, cwd: BACKEND_DIR });
|
|
90
123
|
proc.stdout.on('data', (d) => {
|
|
91
124
|
jobs[jobId].logs += d.toString();
|
|
@@ -94,6 +127,7 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
94
127
|
jobs[jobId].logs += d.toString();
|
|
95
128
|
});
|
|
96
129
|
proc.on('close', (code) => {
|
|
130
|
+
clearInterval(ssPoller);
|
|
97
131
|
jobs[jobId].status = code === 0 ? 'done' : 'error';
|
|
98
132
|
jobs[jobId].exitCode = code;
|
|
99
133
|
|
|
@@ -103,6 +137,8 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
103
137
|
}
|
|
104
138
|
} catch {}
|
|
105
139
|
|
|
140
|
+
fs.rm(ssDir, { recursive: true, force: true }, () => {});
|
|
141
|
+
|
|
106
142
|
if (jobs[jobId].tempTestsDir) {
|
|
107
143
|
fs.rm(jobs[jobId].tempTestsDir, { recursive: true, force: true }, () => {});
|
|
108
144
|
}
|
|
@@ -132,10 +168,12 @@ router.get('/execute/:jobId', authGuard, (req, res) => {
|
|
|
132
168
|
if (!job) return res.status(404).json({ error: 'Job not found' });
|
|
133
169
|
|
|
134
170
|
const offset = parseInt(req.query.offset || '0', 10);
|
|
171
|
+
const screenshots = job.pendingScreenshots.splice(0);
|
|
135
172
|
res.json({
|
|
136
173
|
status: job.status,
|
|
137
174
|
logs: job.logs.slice(offset),
|
|
138
|
-
exitCode: job.exitCode
|
|
175
|
+
exitCode: job.exitCode,
|
|
176
|
+
screenshots
|
|
139
177
|
});
|
|
140
178
|
});
|
|
141
179
|
|
|
@@ -168,8 +168,15 @@ function processCucumberJson(raw) {
|
|
|
168
168
|
const failedStepIndex = visibleSteps.findLastIndex((s) => s.result?.status === 'failed');
|
|
169
169
|
|
|
170
170
|
const steps = visibleSteps.map((step, index) => {
|
|
171
|
+
// AfterStep hook attachments land in step.after[].embeddings in Cucumber.js JSON
|
|
172
|
+
const afterStepScreenshot =
|
|
173
|
+
(step.after ?? []).flatMap(
|
|
174
|
+
(a) => a.embeddings?.filter((e) => e.mime_type === 'image/png') ?? []
|
|
175
|
+
)[0]?.data ?? null;
|
|
176
|
+
|
|
171
177
|
const screenshotData =
|
|
172
178
|
step.embeddings?.find((e) => e.mime_type === 'image/png')?.data ??
|
|
179
|
+
afterStepScreenshot ??
|
|
173
180
|
(index === failedStepIndex ? hookScreenshots[0]?.data : null) ??
|
|
174
181
|
null;
|
|
175
182
|
|
|
@@ -265,6 +272,7 @@ const getReportDetail = async (id) => {
|
|
|
265
272
|
runnerName: true,
|
|
266
273
|
createdAt: true,
|
|
267
274
|
content: true,
|
|
275
|
+
logs: true,
|
|
268
276
|
testRun: { select: { id: true, title: true } }
|
|
269
277
|
}
|
|
270
278
|
});
|
|
@@ -300,7 +308,8 @@ const saveReport = async ({
|
|
|
300
308
|
runnerName,
|
|
301
309
|
runnerId,
|
|
302
310
|
testRunId,
|
|
303
|
-
forceFail = false
|
|
311
|
+
forceFail = false,
|
|
312
|
+
logs = null
|
|
304
313
|
}) => {
|
|
305
314
|
const normTrigger = normaliseTrigger(triggerType);
|
|
306
315
|
const { features, status: derivedStatus } = processCucumberJson(rawCucumberJson);
|
|
@@ -318,7 +327,8 @@ const saveReport = async ({
|
|
|
318
327
|
runnerId: runnerId ?? null,
|
|
319
328
|
cronJobId,
|
|
320
329
|
testRunId: testRunId ?? null,
|
|
321
|
-
content: { features }
|
|
330
|
+
content: { features },
|
|
331
|
+
logs: logs || null
|
|
322
332
|
}
|
|
323
333
|
});
|
|
324
334
|
syncAutomatedTags(report.id, features, testRunId ?? null);
|
|
@@ -345,7 +355,8 @@ const saveCombinedReport = async ({
|
|
|
345
355
|
tag,
|
|
346
356
|
triggerType,
|
|
347
357
|
browser,
|
|
348
|
-
testRunId
|
|
358
|
+
testRunId,
|
|
359
|
+
laneLogs = null
|
|
349
360
|
}) => {
|
|
350
361
|
const featureMap = new Map();
|
|
351
362
|
for (const content of reports) {
|
|
@@ -371,6 +382,14 @@ const saveCombinedReport = async ({
|
|
|
371
382
|
}
|
|
372
383
|
const combined = [...featureMap.values()];
|
|
373
384
|
|
|
385
|
+
let combinedLogs = null;
|
|
386
|
+
if (laneLogs) {
|
|
387
|
+
const parts = runners
|
|
388
|
+
.map((r) => (laneLogs[r.id] ? `=== ${r.name} ===\n${laneLogs[r.id]}` : null))
|
|
389
|
+
.filter(Boolean);
|
|
390
|
+
if (parts.length > 0) combinedLogs = parts.join('\n\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
374
393
|
return saveReport({
|
|
375
394
|
rawCucumberJson: combined,
|
|
376
395
|
tags: tag,
|
|
@@ -380,7 +399,8 @@ const saveCombinedReport = async ({
|
|
|
380
399
|
runnerName: runners.map((r) => r.name).join(', '),
|
|
381
400
|
runnerId: null,
|
|
382
401
|
testRunId: testRunId ?? null,
|
|
383
|
-
forceFail: reports.some((r) => r === null)
|
|
402
|
+
forceFail: reports.some((r) => r === null),
|
|
403
|
+
logs: combinedLogs
|
|
384
404
|
});
|
|
385
405
|
};
|
|
386
406
|
|