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.
Files changed (49) hide show
  1. package/backend/_scaffold/utils/browser.ts +47 -2
  2. package/backend/_scaffold/utils/hooks.ts +25 -2
  3. package/backend/config/scripts/run-tests.js +35 -13
  4. package/backend/logs/runner-cmr8o2x3n0000mg01410tpuck.log +20 -0
  5. package/backend/logs/runner-cmr90hvcx0000n2016edtmrtv.log +43 -0
  6. package/backend/logs/runner-cmr90i7h20001n201g2wg6v3n.log +43 -0
  7. package/backend/logs/runner-cmr90iilm0002n201mmisva95.log +43 -0
  8. package/backend/logs/runner-cmr90j70r0003n201xz8525dz.log +20 -0
  9. package/backend/logs/runner-cmr90yup70000qp017ianc2gp.log +43 -0
  10. package/backend/logs/runner-cmr90zbtm0001qp01u9tfdkh1.log +20 -0
  11. package/backend/logs/runner-cmr91a3mw0000tb01414rd1df.log +20 -0
  12. package/backend/logs/runner-cmr91dhla0001tb01jp7thjnl.log +20 -0
  13. package/backend/prisma/migrations/20260706000000_add_report_logs/migration.sql +2 -0
  14. package/backend/prisma/schema.prisma +1 -0
  15. package/backend/routes/node.routes.js +42 -4
  16. package/backend/services/reportService.js +24 -4
  17. package/backend/services/runnerService.js +11 -1
  18. package/backend/websockets/socketHandler.js +162 -16
  19. package/frontend/package.json +1 -1
  20. package/frontend/src/lib/components/layout/RunnerPanel.svelte +53 -55
  21. package/frontend/src/lib/components/ui/AutomatedBadge.svelte +56 -0
  22. package/frontend/src/lib/components/ui/BackLink.svelte +77 -0
  23. package/frontend/src/lib/components/ui/Badge.svelte +1 -1
  24. package/frontend/src/lib/components/ui/Button.svelte +1 -1
  25. package/frontend/src/lib/components/ui/CaseIdChip.svelte +59 -0
  26. package/frontend/src/lib/components/ui/ConfirmModal.svelte +1 -1
  27. package/frontend/src/lib/components/ui/Pagination.svelte +1 -1
  28. package/frontend/src/lib/components/ui/PriorityBadge.svelte +83 -0
  29. package/frontend/src/lib/components/ui/ResultChip.svelte +92 -0
  30. package/frontend/src/lib/components/ui/StatusDot.svelte +64 -0
  31. package/frontend/src/lib/components/ui/StepKeyword.svelte +79 -0
  32. package/frontend/src/lib/components/ui/StepStatusIcon.svelte +66 -0
  33. package/frontend/src/lib/components/ui/TagChip.svelte +51 -0
  34. package/frontend/src/lib/constants.js +14 -0
  35. package/frontend/src/lib/stores/runner.js +5 -3
  36. package/frontend/src/lib/styles/tokens.css +26 -0
  37. package/frontend/src/lib/utils/format.js +74 -0
  38. package/frontend/src/routes/+page.svelte +3 -3
  39. package/frontend/src/routes/login/+page.svelte +1 -1
  40. package/frontend/src/routes/reports/+page.svelte +3 -3
  41. package/frontend/src/routes/reports/[id]/+page.svelte +703 -215
  42. package/frontend/src/routes/reports/live/+page.svelte +281 -283
  43. package/frontend/src/routes/scheduled-tests/+page.svelte +3 -3
  44. package/frontend/src/routes/settings/+page.svelte +6 -6
  45. package/frontend/src/routes/setup/+page.svelte +1 -1
  46. package/frontend/src/routes/test-repository/+page.svelte +5 -5
  47. package/frontend/src/routes/test-repository/runs/[id]/+page.svelte +27 -151
  48. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +4 -4
  49. 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
- import { Before, After, ITestCaseHookParameter } from '@cucumber/cucumber';
2
- import { setup, teardown } from './browser';
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
- `${testsRoot}/features/**/*.feature`
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. The node's
59
- // own working directory ships a cucumber.json whose `require` globs point at the
60
- // node's *local* tests/ and cucumber merges config-file `require` additively with
61
- // the CLI ones, so every step would match two definitions (local + dispatched) and
62
- // run as "ambiguous": never executing, reporting zero duration. Point cucumber at a
63
- // self-contained config inside the dispatched dir so the node's own cucumber.json is
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
- const configPath = path.join(testsRootAbs, 'cucumber.json');
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
- configPath,
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)
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE "Report" ADD COLUMN "logs" TEXT;
@@ -67,6 +67,7 @@ model Report {
67
67
  testRunId String?
68
68
  testRun TestRun? @relation(fields: [testRunId], references: [id], onDelete: SetNull)
69
69
  content Json
70
+ logs String?
70
71
  createdAt DateTime @default(now())
71
72
  testHistory TestCaseHistory[]
72
73
  }
@@ -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(os.tmpdir(), `plum-job-${jobId}`);
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(os.tmpdir(), `plum-report-${jobId}.json`);
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