plum-e2e 2.5.9 → 2.5.11

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.
@@ -67,6 +67,7 @@ async function api(method, path, body) {
67
67
  const get = (path) => api('GET', path);
68
68
  const post = (path, body) => api('POST', path, body);
69
69
  const put = (path, body) => api('PUT', path, body);
70
+ const del = (path) => api('DELETE', path);
70
71
 
71
72
  // ---------------------------------------------------------------------------
72
73
  // Polling helper for test runs
@@ -176,6 +177,33 @@ server.tool(
176
177
  }
177
178
  );
178
179
 
180
+ server.tool(
181
+ 'update_test_suite',
182
+ "Update a test suite's name, description, or priority. Only the fields provided are changed.",
183
+ {
184
+ suiteId: z.string().describe('UUID of the suite to update'),
185
+ name: z.string().min(1).optional(),
186
+ description: z.string().optional(),
187
+ priority: z.enum(['Critical', 'High', 'Medium', 'Low']).optional()
188
+ },
189
+ async ({ suiteId, name, description, priority }) => {
190
+ const data = await put(`/test-suites/${suiteId}`, { name, description, priority });
191
+ return { content: [{ type: 'text', text: JSON.stringify(data.suite, null, 2) }] };
192
+ }
193
+ );
194
+
195
+ server.tool(
196
+ 'delete_test_suite',
197
+ 'Permanently delete a test suite and all of its test cases. This cannot be undone.',
198
+ {
199
+ suiteId: z.string().describe('UUID of the suite to delete')
200
+ },
201
+ async ({ suiteId }) => {
202
+ await del(`/test-suites/${suiteId}`);
203
+ return { content: [{ type: 'text', text: `Suite ${suiteId} deleted.` }] };
204
+ }
205
+ );
206
+
179
207
  // -- Test Repository: Cases -------------------------------------------------
180
208
 
181
209
  server.tool(
@@ -193,6 +221,45 @@ server.tool(
193
221
  }
194
222
  );
195
223
 
224
+ server.tool(
225
+ 'get_test_case',
226
+ 'Get a test case by ID, including its manual steps and recent execution history.',
227
+ {
228
+ caseId: z.string().describe('UUID of the test case')
229
+ },
230
+ async ({ caseId }) => {
231
+ const data = await get(`/test-cases/${caseId}`);
232
+ return { content: [{ type: 'text', text: JSON.stringify(data.testCase, null, 2) }] };
233
+ }
234
+ );
235
+
236
+ server.tool(
237
+ 'update_test_case',
238
+ "Update a test case's title, description, or priority. Only the fields provided are changed.",
239
+ {
240
+ caseId: z.string().describe('UUID of the test case to update'),
241
+ title: z.string().min(1).optional(),
242
+ description: z.string().optional(),
243
+ priority: z.enum(['Critical', 'High', 'Medium', 'Low']).optional()
244
+ },
245
+ async ({ caseId, title, description, priority }) => {
246
+ const data = await put(`/test-cases/${caseId}`, { title, description, priority });
247
+ return { content: [{ type: 'text', text: JSON.stringify(data.testCase, null, 2) }] };
248
+ }
249
+ );
250
+
251
+ server.tool(
252
+ 'delete_test_case',
253
+ 'Permanently delete a test case and its steps. This cannot be undone.',
254
+ {
255
+ caseId: z.string().describe('UUID of the test case to delete')
256
+ },
257
+ async ({ caseId }) => {
258
+ await del(`/test-cases/${caseId}`);
259
+ return { content: [{ type: 'text', text: `Test case ${caseId} deleted.` }] };
260
+ }
261
+ );
262
+
196
263
  server.tool(
197
264
  'set_test_steps',
198
265
  'Set (replace) the manual test steps for a test case. Each step has an action, optional test data, and optional expected output.',
@@ -32,7 +32,7 @@ router.get('/', async (req, res) => {
32
32
  router.post('/', async (req, res) => {
33
33
  try {
34
34
  const { cronExpression, taskName, tags } = req.body;
35
- if (!cronExpression || !taskName || !tags) {
35
+ if (!cronExpression || !taskName) {
36
36
  return res.status(400).json({ error: 'Missing required fields' });
37
37
  }
38
38
  await cronService.addCronJob(req.body);
@@ -51,7 +51,7 @@ router.put('/:taskName', async (req, res) => {
51
51
  try {
52
52
  const { taskName } = req.params;
53
53
  const { cronExpression, tags } = req.body;
54
- if (!cronExpression || !tags) {
54
+ if (!cronExpression) {
55
55
  return res.status(400).json({ error: 'Missing required fields' });
56
56
  }
57
57
  await cronService.updateCronJob(taskName, req.body);
@@ -284,7 +284,7 @@ const addCronJob = async ({
284
284
  notifyDiscord,
285
285
  notifySlack
286
286
  }) => {
287
- if (!cronExpression || !taskName || !tags) {
287
+ if (!cronExpression || !taskName) {
288
288
  return { status: 400, message: 'Missing required parameters' };
289
289
  }
290
290
  const runnerIdsStr =
@@ -294,7 +294,7 @@ const addCronJob = async ({
294
294
  data: {
295
295
  taskName,
296
296
  cronExpression,
297
- tags,
297
+ tags: tags ?? '',
298
298
  workers: workers ?? 1,
299
299
  browser: browser ?? 'chromium',
300
300
  runnerIds: runnerIdsStr,
@@ -105,10 +105,27 @@ async function update(id, { title, status, caseIds }) {
105
105
  if (status !== undefined) data.status = status;
106
106
 
107
107
  if (caseIds !== undefined) {
108
- await prisma.testRunEntry.deleteMany({ where: { runId: id } });
108
+ // Diff against existing entries instead of delete-all/recreate, so cases that
109
+ // remain in the run keep their recorded status, notes, and assignment.
110
+ const existing = await prisma.testRunEntry.findMany({
111
+ where: { runId: id },
112
+ select: { id: true, caseId: true }
113
+ });
114
+ const existingIdByCaseId = new Map(existing.map((e) => [e.caseId, e.id]));
115
+ const keepCaseIds = new Set(caseIds);
116
+ const removedIds = existing.filter((e) => !keepCaseIds.has(e.caseId)).map((e) => e.id);
117
+
118
+ if (removedIds.length > 0) {
119
+ await prisma.testRunEntry.deleteMany({ where: { id: { in: removedIds } } });
120
+ }
109
121
  await prisma.$transaction(
110
122
  caseIds.map((caseId, i) =>
111
- prisma.testRunEntry.create({ data: { runId: id, caseId, order: i } })
123
+ existingIdByCaseId.has(caseId)
124
+ ? prisma.testRunEntry.update({
125
+ where: { id: existingIdByCaseId.get(caseId) },
126
+ data: { order: i }
127
+ })
128
+ : prisma.testRunEntry.create({ data: { runId: id, caseId, order: i } })
112
129
  )
113
130
  );
114
131
  }
@@ -43,55 +43,56 @@
43
43
  * ```
44
44
  */
45
45
  declare module '$env/static/private' {
46
- export const CODEX_SANDBOX_NETWORK_DISABLED: string;
46
+ export const NoDefaultCurrentDirectoryInExePath: string;
47
+ export const CLAUDE_EFFORT: string;
48
+ export const CLAUDE_CODE_ENTRYPOINT: string;
47
49
  export const TERM_PROGRAM: string;
48
50
  export const NODE: string;
49
51
  export const INIT_CWD: string;
52
+ export const VIKUNJA_BASE_URL: string;
50
53
  export const TERM: string;
51
54
  export const SHELL: string;
55
+ export const CLAUDE_CODE_CHILD_SESSION: string;
52
56
  export const TMPDIR: string;
53
57
  export const npm_config_global_prefix: string;
54
- export const CODEX_MANAGED_PACKAGE_ROOT: string;
55
58
  export const VSCODE_PYTHON_AUTOACTIVATE_GUARD: string;
56
59
  export const TERM_PROGRAM_VERSION: string;
57
60
  export const ZDOTDIR: string;
58
61
  export const MallocNanoZone: string;
59
62
  export const COLOR: string;
60
- export const NO_COLOR: string;
61
63
  export const npm_config_noproxy: string;
62
64
  export const npm_config_local_prefix: string;
63
- export const LC_ALL: string;
65
+ export const GIT_EDITOR: string;
66
+ export const AI_AGENT: string;
64
67
  export const COPILOT_DEBUG_NONCE: string;
68
+ export const VIKUNJA_API_KEY: string;
65
69
  export const USER: string;
66
70
  export const COMMAND_MODE: string;
67
71
  export const npm_config_globalconfig: string;
68
- export const SSH_AUTH_SOCK: string;
72
+ export const OUTLINE_BASE_URL: string;
69
73
  export const CLAUDE_CODE_SSE_PORT: string;
70
- export const __CF_USER_TEXT_ENCODING: string;
74
+ export const SSH_AUTH_SOCK: string;
71
75
  export const VSCODE_PROFILE_INITIALIZED: string;
76
+ export const __CF_USER_TEXT_ENCODING: string;
72
77
  export const npm_execpath: string;
73
- export const PAGER: string;
74
78
  export const PATH: string;
75
- export const CODEX_SANDBOX: string;
76
79
  export const npm_package_json: string;
77
80
  export const npm_config_engine_strict: string;
78
81
  export const _: string;
79
82
  export const npm_config_userconfig: string;
80
83
  export const npm_config_init_module: string;
81
- export const __CFBundleIdentifier: string;
82
84
  export const USER_ZDOTDIR: string;
83
- export const CODEX_THREAD_ID: string;
85
+ export const __CFBundleIdentifier: string;
84
86
  export const npm_command: string;
85
87
  export const PWD: string;
86
88
  export const npm_lifecycle_event: string;
87
89
  export const EDITOR: string;
90
+ export const OUTLINE_API_KEY: string;
88
91
  export const npm_package_name: string;
89
92
  export const LANG: string;
90
93
  export const npm_config_npm_version: string;
91
- export const XPC_FLAGS: string;
92
94
  export const VSCODE_GIT_ASKPASS_EXTRA_ARGS: string;
93
- export const CODEX_MANAGED_BY_NPM: string;
94
- export const CODEX_CI: string;
95
+ export const XPC_FLAGS: string;
95
96
  export const npm_config_node_gyp: string;
96
97
  export const npm_package_version: string;
97
98
  export const XPC_SERVICE_NAME: string;
@@ -99,17 +100,18 @@ declare module '$env/static/private' {
99
100
  export const SHLVL: string;
100
101
  export const HOME: string;
101
102
  export const VSCODE_GIT_ASKPASS_MAIN: string;
102
- export const GH_PAGER: string;
103
+ export const CLAUDE_CODE_EXECPATH: string;
103
104
  export const npm_config_cache: string;
104
105
  export const LOGNAME: string;
105
106
  export const npm_lifecycle_script: string;
106
107
  export const VSCODE_GIT_IPC_HANDLE: string;
107
- export const LC_CTYPE: string;
108
+ export const COREPACK_ENABLE_AUTO_PIN: string;
108
109
  export const npm_config_user_agent: string;
110
+ export const CLAUDE_CODE_SESSION_ID: string;
109
111
  export const VSCODE_GIT_ASKPASS_NODE: string;
110
112
  export const GIT_ASKPASS: string;
111
113
  export const OSLogRateLimit: string;
112
- export const GIT_PAGER: string;
114
+ export const CLAUDECODE: string;
113
115
  export const npm_node_execpath: string;
114
116
  export const npm_config_prefix: string;
115
117
  export const COLORTERM: string;
@@ -145,55 +147,56 @@ declare module '$env/static/public' {
145
147
  */
146
148
  declare module '$env/dynamic/private' {
147
149
  export const env: {
148
- CODEX_SANDBOX_NETWORK_DISABLED: string;
150
+ NoDefaultCurrentDirectoryInExePath: string;
151
+ CLAUDE_EFFORT: string;
152
+ CLAUDE_CODE_ENTRYPOINT: string;
149
153
  TERM_PROGRAM: string;
150
154
  NODE: string;
151
155
  INIT_CWD: string;
156
+ VIKUNJA_BASE_URL: string;
152
157
  TERM: string;
153
158
  SHELL: string;
159
+ CLAUDE_CODE_CHILD_SESSION: string;
154
160
  TMPDIR: string;
155
161
  npm_config_global_prefix: string;
156
- CODEX_MANAGED_PACKAGE_ROOT: string;
157
162
  VSCODE_PYTHON_AUTOACTIVATE_GUARD: string;
158
163
  TERM_PROGRAM_VERSION: string;
159
164
  ZDOTDIR: string;
160
165
  MallocNanoZone: string;
161
166
  COLOR: string;
162
- NO_COLOR: string;
163
167
  npm_config_noproxy: string;
164
168
  npm_config_local_prefix: string;
165
- LC_ALL: string;
169
+ GIT_EDITOR: string;
170
+ AI_AGENT: string;
166
171
  COPILOT_DEBUG_NONCE: string;
172
+ VIKUNJA_API_KEY: string;
167
173
  USER: string;
168
174
  COMMAND_MODE: string;
169
175
  npm_config_globalconfig: string;
170
- SSH_AUTH_SOCK: string;
176
+ OUTLINE_BASE_URL: string;
171
177
  CLAUDE_CODE_SSE_PORT: string;
172
- __CF_USER_TEXT_ENCODING: string;
178
+ SSH_AUTH_SOCK: string;
173
179
  VSCODE_PROFILE_INITIALIZED: string;
180
+ __CF_USER_TEXT_ENCODING: string;
174
181
  npm_execpath: string;
175
- PAGER: string;
176
182
  PATH: string;
177
- CODEX_SANDBOX: string;
178
183
  npm_package_json: string;
179
184
  npm_config_engine_strict: string;
180
185
  _: string;
181
186
  npm_config_userconfig: string;
182
187
  npm_config_init_module: string;
183
- __CFBundleIdentifier: string;
184
188
  USER_ZDOTDIR: string;
185
- CODEX_THREAD_ID: string;
189
+ __CFBundleIdentifier: string;
186
190
  npm_command: string;
187
191
  PWD: string;
188
192
  npm_lifecycle_event: string;
189
193
  EDITOR: string;
194
+ OUTLINE_API_KEY: string;
190
195
  npm_package_name: string;
191
196
  LANG: string;
192
197
  npm_config_npm_version: string;
193
- XPC_FLAGS: string;
194
198
  VSCODE_GIT_ASKPASS_EXTRA_ARGS: string;
195
- CODEX_MANAGED_BY_NPM: string;
196
- CODEX_CI: string;
199
+ XPC_FLAGS: string;
197
200
  npm_config_node_gyp: string;
198
201
  npm_package_version: string;
199
202
  XPC_SERVICE_NAME: string;
@@ -201,17 +204,18 @@ declare module '$env/dynamic/private' {
201
204
  SHLVL: string;
202
205
  HOME: string;
203
206
  VSCODE_GIT_ASKPASS_MAIN: string;
204
- GH_PAGER: string;
207
+ CLAUDE_CODE_EXECPATH: string;
205
208
  npm_config_cache: string;
206
209
  LOGNAME: string;
207
210
  npm_lifecycle_script: string;
208
211
  VSCODE_GIT_IPC_HANDLE: string;
209
- LC_CTYPE: string;
212
+ COREPACK_ENABLE_AUTO_PIN: string;
210
213
  npm_config_user_agent: string;
214
+ CLAUDE_CODE_SESSION_ID: string;
211
215
  VSCODE_GIT_ASKPASS_NODE: string;
212
216
  GIT_ASKPASS: string;
213
217
  OSLogRateLimit: string;
214
- GIT_PAGER: string;
218
+ CLAUDECODE: string;
215
219
  npm_node_execpath: string;
216
220
  npm_config_prefix: string;
217
221
  COLORTERM: string;
@@ -23,16 +23,30 @@ export const nodes = [
23
23
  () => import('./nodes/2'),
24
24
  () => import('./nodes/3'),
25
25
  () => import('./nodes/4'),
26
- () => import('./nodes/5')
26
+ () => import('./nodes/5'),
27
+ () => import('./nodes/6'),
28
+ () => import('./nodes/7'),
29
+ () => import('./nodes/8'),
30
+ () => import('./nodes/9'),
31
+ () => import('./nodes/10'),
32
+ () => import('./nodes/11'),
33
+ () => import('./nodes/12')
27
34
  ];
28
35
 
29
36
  export const server_loads = [];
30
37
 
31
38
  export const dictionary = {
32
39
  "/": [2],
33
- "/reports": [3],
34
- "/reports/[slug]": [4],
35
- "/scheduled-tests": [5]
40
+ "/login": [3],
41
+ "/reports": [4],
42
+ "/reports/live": [6],
43
+ "/reports/[id]": [5],
44
+ "/scheduled-tests": [7],
45
+ "/settings": [8],
46
+ "/setup": [9],
47
+ "/test-repository": [10],
48
+ "/test-repository/runs/[id]": [11],
49
+ "/test-repository/suites/[id]": [12]
36
50
  };
37
51
 
38
52
  export const hooks = {
@@ -15,4 +15,6 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
+ import * as universal from "../../../../src/routes/+layout.js";
19
+ export { universal };
18
20
  export { default as component } from "../../../../src/routes/+layout.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/test-repository/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/test-repository/runs/[id]/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/test-repository/suites/[id]/+page.svelte";
@@ -15,4 +15,4 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- export { default as component } from "../../../../src/routes/reports/+page.svelte";
18
+ export { default as component } from "../../../../src/routes/login/+page.svelte";
@@ -15,4 +15,4 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- export { default as component } from "../../../../src/routes/reports/[slug]/+page.svelte";
18
+ export { default as component } from "../../../../src/routes/reports/+page.svelte";
@@ -15,4 +15,4 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- export { default as component } from "../../../../src/routes/scheduled-tests/+page.svelte";
18
+ export { default as component } from "../../../../src/routes/reports/[id]/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/reports/live/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/scheduled-tests/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/settings/+page.svelte";
@@ -0,0 +1,18 @@
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
+ export { default as component } from "../../../../src/routes/setup/+page.svelte";
@@ -35,10 +35,10 @@ export const options = {
35
35
  root,
36
36
  service_worker: false,
37
37
  templates: {
38
- app: ({ head, body, assets, nonce, env }) => "<!--\nThis file is part of Plum.\n\nPlum is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nPlum is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with Plum. If not, see https://www.gnu.org/licenses/.\n-->\n<!doctype html>\n<html lang=\"en\" data-theme=\"light\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n\t\t<link\n\t\t\thref=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap\"\n\t\t\trel=\"stylesheet\"\n\t\t/>\n\t\t<!-- Prevent theme flash before Svelte hydrates -->\n\t\t<script>\n\t\t\ttry {\n\t\t\t\tconst t = localStorage.getItem('plum-theme');\n\t\t\t\tif (t) document.documentElement.setAttribute('data-theme', t);\n\t\t\t} catch (e) {}\n\t\t</script>\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
38
+ app: ({ head, body, assets, nonce, env }) => "<!--\nThis file is part of Plum.\n\nPlum is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nPlum is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with Plum. If not, see https://www.gnu.org/licenses/.\n-->\n<!doctype html>\n<html lang=\"en\" data-theme=\"light\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.ico\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n\t\t<link\n\t\t\thref=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap\"\n\t\t\trel=\"stylesheet\"\n\t\t/>\n\t\t<!-- Prevent theme flash before Svelte hydrates -->\n\t\t<script>\n\t\t\ttry {\n\t\t\t\tconst t = localStorage.getItem('plum-theme');\n\t\t\t\tif (t) document.documentElement.setAttribute('data-theme', t);\n\t\t\t} catch (e) {}\n\t\t</script>\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
39
39
  error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
40
40
  },
41
- version_hash: "kg35is"
41
+ version_hash: "1r4g2tv"
42
42
  };
43
43
 
44
44
  export async function get_hooks() {
@@ -82,7 +82,10 @@
82
82
  <style>
83
83
  .backdrop {
84
84
  position: fixed;
85
- inset: 0;
85
+ top: 0;
86
+ left: 0;
87
+ right: 0;
88
+ bottom: var(--bottom-bar-height);
86
89
  background: rgba(0, 0, 0, 0.45);
87
90
  display: flex;
88
91
  align-items: center;
@@ -90,6 +93,7 @@
90
93
  z-index: 50;
91
94
  padding: 1rem;
92
95
  backdrop-filter: blur(2px);
96
+ overflow-y: auto;
93
97
  }
94
98
 
95
99
  .panel {
@@ -99,6 +103,8 @@
99
103
  padding: 1.75rem;
100
104
  width: 100%;
101
105
  max-width: 480px;
106
+ max-height: 100%;
107
+ overflow-y: auto;
102
108
  box-shadow:
103
109
  0 4px 6px rgba(0, 0, 0, 0.04),
104
110
  0 24px 64px rgba(0, 0, 0, 0.14);
@@ -37,7 +37,6 @@ export const TOAST_TIMEOUT_MS = 4000;
37
37
 
38
38
  export const REPLAY_STEP_MS = 900;
39
39
  export const REDIRECT_DELAY_MS = 3000;
40
- export const RUN_REFRESH_MS = 15000;
41
40
 
42
41
  export const WORKERS_MIN = 1;
43
42
  export const WORKERS_MAX = 10;
@@ -51,6 +51,8 @@
51
51
  --radius-pill: 100px;
52
52
 
53
53
  --white: #ffffff;
54
+
55
+ --bottom-bar-height: 55px;
54
56
  }
55
57
 
56
58
  @keyframes fadeUp {
@@ -175,7 +175,7 @@
175
175
  }
176
176
 
177
177
  async function handleSave() {
178
- if (!form.taskName || !form.cronExpression || !form.tags) {
178
+ if (!form.taskName || !form.cronExpression) {
179
179
  formError = 'All fields are required.';
180
180
  return;
181
181
  }
@@ -306,14 +306,13 @@
306
306
  <div class="field">
307
307
  <div class="field-label">
308
308
  <span>Tags</span>
309
- <span class="field-hint">Multiple: @test-1 or @test-2</span>
309
+ <span class="field-hint">Multiple: @test-1 or @test-2. Leave blank to run all tests</span>
310
310
  </div>
311
311
  <input
312
312
  type="text"
313
313
  class="field-input"
314
314
  bind:value={form.tags}
315
- placeholder="@suite-login"
316
- required
315
+ placeholder="@suite-login (optional)"
317
316
  />
318
317
  </div>
319
318
 
@@ -16,7 +16,7 @@
16
16
  -->
17
17
 
18
18
  <script>
19
- import { onMount, onDestroy } from 'svelte';
19
+ import { onMount } from 'svelte';
20
20
  import { page } from '$app/stores';
21
21
  import { fly } from 'svelte/transition';
22
22
  import {
@@ -36,7 +36,7 @@
36
36
  import PriorityBadge from '$lib/components/ui/PriorityBadge.svelte';
37
37
  import CaseIdChip from '$lib/components/ui/CaseIdChip.svelte';
38
38
  import ResultChip from '$lib/components/ui/ResultChip.svelte';
39
- import { TOAST_TIMEOUT_MS, RUN_REFRESH_MS } from '$lib/constants';
39
+ import { TOAST_TIMEOUT_MS } from '$lib/constants';
40
40
 
41
41
  const runId = $page.params.id;
42
42
 
@@ -117,8 +117,6 @@
117
117
  setTimeout(() => (toast = null), TOAST_TIMEOUT_MS);
118
118
  }
119
119
 
120
- let refreshInterval = null;
121
-
122
120
  onMount(async () => {
123
121
  try {
124
122
  [run, suites, members] = await Promise.all([
@@ -133,16 +131,6 @@
133
131
  } finally {
134
132
  loading = false;
135
133
  }
136
- refreshInterval = setInterval(async () => {
137
- try {
138
- const fresh = await fetchRun(runId);
139
- run = fresh;
140
- } catch {}
141
- }, RUN_REFRESH_MS);
142
- });
143
-
144
- onDestroy(() => {
145
- if (refreshInterval) clearInterval(refreshInterval);
146
134
  });
147
135
 
148
136
  async function handleAssignEntry(entryId, userId) {
@@ -188,8 +176,9 @@
188
176
  suite.name.toLowerCase().includes(search.toLowerCase())
189
177
  );
190
178
 
191
- function addCase(tc) {
179
+ async function addCase(tc) {
192
180
  if (!run || runCaseIds.has(tc.id)) return;
181
+ const previous = run;
193
182
  const entry = {
194
183
  id: `tmp-${tc.id}`,
195
184
  order: run.entries.length,
@@ -200,6 +189,14 @@
200
189
  case: tc
201
190
  };
202
191
  run = { ...run, entries: [...run.entries, entry] };
192
+ try {
193
+ const caseIds = run.entries.map((e) => e.case.id);
194
+ await updateRun(runId, { caseIds });
195
+ run = await fetchRun(runId);
196
+ } catch (e) {
197
+ run = previous;
198
+ showToast('error', 'Failed to add case.');
199
+ }
203
200
  }
204
201
 
205
202
  function removeEntry(entryId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.5.9",
3
+ "version": "2.5.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"