plum-e2e 2.5.6 → 2.5.8
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/config/scripts/run-tests.js +7 -12
- package/backend/lib/testEnv.js +38 -0
- package/backend/routes/node.routes.js +6 -1
- package/backend/services/runnerService.js +8 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +4 -2
- package/frontend/src/lib/utils/format.js +14 -3
- package/package.json +1 -1
|
@@ -69,18 +69,13 @@ try {
|
|
|
69
69
|
|
|
70
70
|
// hooks.ts calls dotenv.config() with no path, which defaults to process.cwd().
|
|
71
71
|
// When running from a temp dir there is no .env there, so vars like BASE_URL
|
|
72
|
-
// would be undefined.
|
|
73
|
-
// process.env
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
}
|
|
72
|
+
// would be undefined. A dispatched node job already has these injected into
|
|
73
|
+
// process.env by node.routes.js from the primary's payload; this is just a
|
|
74
|
+
// fallback for local/standalone runs, so it never overwrites what's already set.
|
|
75
|
+
const { loadTestEnv } = require('../../lib/testEnv');
|
|
76
|
+
const backendEnv = loadTestEnv(path.resolve(__dirname, '..', '..'));
|
|
77
|
+
for (const [key, val] of Object.entries(backendEnv)) {
|
|
78
|
+
if (!(key in process.env)) process.env[key] = val;
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
fs.writeFileSync(
|
|
@@ -0,0 +1,38 @@
|
|
|
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 fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses a dir's `.env` into a plain object. Used to hand a remote runner node
|
|
23
|
+
* the same BASE_URL/IS_HEADLESS/custom test vars the primary has, so nodes stay
|
|
24
|
+
* stateless runners instead of needing their own local `.env`.
|
|
25
|
+
*/
|
|
26
|
+
function loadTestEnv(dir) {
|
|
27
|
+
const out = {};
|
|
28
|
+
try {
|
|
29
|
+
const txt = fs.readFileSync(path.join(dir, '.env'), 'utf8');
|
|
30
|
+
for (const line of txt.split(/\r?\n/)) {
|
|
31
|
+
const m = line.match(/^([A-Za-z_]\w*)\s*=\s*(.*?)(\s*#.*)?$/);
|
|
32
|
+
if (m) out[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { loadTestEnv };
|
|
@@ -47,7 +47,7 @@ router.post('/shutdown', authGuard, (req, res) => {
|
|
|
47
47
|
|
|
48
48
|
// Start a remote test job
|
|
49
49
|
router.post('/execute', authGuard, (req, res) => {
|
|
50
|
-
const { tags, browser = 'chromium', workers = 1, tests = null } = req.body;
|
|
50
|
+
const { tags, browser = 'chromium', workers = 1, tests = null, env: userEnv = {} } = req.body;
|
|
51
51
|
const jobId = crypto.randomUUID();
|
|
52
52
|
|
|
53
53
|
// path.resolve ensures absolute even if TMPDIR env var is set to a relative path
|
|
@@ -84,6 +84,11 @@ router.post('/execute', authGuard, (req, res) => {
|
|
|
84
84
|
|
|
85
85
|
const env = {
|
|
86
86
|
...process.env,
|
|
87
|
+
// User/test vars (BASE_URL, IS_HEADLESS, custom secrets) forwarded from the
|
|
88
|
+
// primary's own .env — nodes are stateless runners and shouldn't need their
|
|
89
|
+
// own copy. Spread before the job-control vars below so a stray same-named
|
|
90
|
+
// var in the user's .env can never override how this job actually runs.
|
|
91
|
+
...userEnv,
|
|
87
92
|
TAG: tags || '',
|
|
88
93
|
TRIGGER: TRIGGER_REMOTE,
|
|
89
94
|
BROWSER: browser,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const prisma = require('./prisma');
|
|
21
|
+
const { loadTestEnv } = require('../lib/testEnv');
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Runner CRUD
|
|
@@ -170,7 +171,13 @@ async function dispatchAndPoll(
|
|
|
170
171
|
'Content-Type': 'application/json',
|
|
171
172
|
Authorization: `Bearer ${runner.token}`
|
|
172
173
|
},
|
|
173
|
-
body: JSON.stringify({
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
tags,
|
|
176
|
+
browser,
|
|
177
|
+
workers,
|
|
178
|
+
tests: collectTestFiles(),
|
|
179
|
+
env: loadTestEnv(process.cwd())
|
|
180
|
+
}),
|
|
174
181
|
signal: AbortSignal.timeout(10000)
|
|
175
182
|
});
|
|
176
183
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
@@ -123,7 +123,9 @@
|
|
|
123
123
|
runnerConfig.update((c) => {
|
|
124
124
|
if (!v && c.selectedRunners.includes(BUILTIN_RUNNER_ID)) {
|
|
125
125
|
const others = c.selectedRunners.filter((r) => r !== BUILTIN_RUNNER_ID);
|
|
126
|
-
|
|
126
|
+
const fallback =
|
|
127
|
+
others.length > 0 ? others : availableRunners[0] ? [availableRunners[0].id] : [];
|
|
128
|
+
return { ...c, selectedRunners: fallback };
|
|
127
129
|
}
|
|
128
130
|
return c;
|
|
129
131
|
});
|
|
@@ -601,7 +603,7 @@
|
|
|
601
603
|
</button>
|
|
602
604
|
{#if runnersOpen}
|
|
603
605
|
<div class="dropdown-menu runners-menu" transition:fly={{ y: 6, duration: 130 }}>
|
|
604
|
-
{#if $builtInEnabled}
|
|
606
|
+
{#if $builtInEnabled || isRunnerSelected(BUILTIN_RUNNER_ID)}
|
|
605
607
|
<label class="runner-option">
|
|
606
608
|
<input
|
|
607
609
|
type="checkbox"
|
|
@@ -73,13 +73,24 @@ export function keywordClass(kw) {
|
|
|
73
73
|
return 'kw-and';
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Matches both verbose tags (@test-login-1, @suite-login) and the TC-/TS-
|
|
77
|
+
// displayId convention (@TC-001, @TS-001) used by default throughout the rest
|
|
78
|
+
// of the app (test case/suite prefixes, configurable in Settings).
|
|
79
|
+
function isSuiteTag(tag) {
|
|
80
|
+
return /suite/i.test(tag) || /^@ts-?\d+/i.test(tag);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isTestCaseTag(tag) {
|
|
84
|
+
return /^@test[\w-]*/i.test(tag) || /^@tc-?\d+/i.test(tag);
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
export function scenarioTestTag(scenario) {
|
|
77
|
-
return scenario.tags?.find(
|
|
88
|
+
return scenario.tags?.find(isTestCaseTag) ?? null;
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export function featureSuiteTag(feature) {
|
|
81
92
|
for (const scenario of feature.scenarios ?? []) {
|
|
82
|
-
const suiteTag = scenario.tags?.find(
|
|
93
|
+
const suiteTag = scenario.tags?.find(isSuiteTag);
|
|
83
94
|
if (suiteTag) return suiteTag;
|
|
84
95
|
}
|
|
85
96
|
return null;
|
|
@@ -88,7 +99,7 @@ export function featureSuiteTag(feature) {
|
|
|
88
99
|
export function visibleTags(scenario) {
|
|
89
100
|
const testTag = scenarioTestTag(scenario);
|
|
90
101
|
if (testTag) return [testTag];
|
|
91
|
-
return (scenario.tags ?? []).filter((tag) =>
|
|
102
|
+
return (scenario.tags ?? []).filter((tag) => !isSuiteTag(tag));
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
const STATUS_RANK = { failed: 3, pending: 2, skipped: 1, passed: 0 };
|