|
13 | 13 | } from '@lucide/svelte'; |
14 | 14 | import Modal from './Modal.svelte'; |
15 | 15 | import Button from '../components/Button.svelte'; |
| 16 | + import Card from '../components/Card.svelte'; |
| 17 | + import Panel from '../components/Panel.svelte'; |
| 18 | + import EmptyState from '../components/EmptyState.svelte'; |
| 19 | + import StateDisplay from '../components/StateDisplay.svelte'; |
| 20 | + import AlertBox from '../components/AlertBox.svelte'; |
| 21 | + import Lozenge from '../components/Lozenge.svelte'; |
| 22 | + import PageHeader from '../layout/PageHeader.svelte'; |
16 | 23 | import { api } from '../api.js'; |
17 | 24 | import { formatAuthenticatedDateTime as formatDateTimeLocale } from '../utils/authenticatedDateFormatter.js'; |
18 | 25 | import { t } from '../stores/i18n.svelte.js'; |
|
22 | 29 | testCaseId = null, |
23 | 30 | workspaceId = null, |
24 | 31 | embedded = false, |
| 32 | + backLabel = null, |
25 | 33 | onclose = null |
26 | 34 | } = $props(); |
27 | 35 |
|
|
92 | 100 | onclose?.(); |
93 | 101 | } |
94 | 102 |
|
95 | | - function getStatusPillStyle(status) { |
96 | | - if (!status) { |
97 | | - return 'background-color: var(--ds-background-neutral); color: var(--ds-text-subtle);'; |
98 | | - } |
| 103 | + function getStatusColor(status) { |
| 104 | + if (!status) return 'gray'; |
99 | 105 | const normalized = status.toLowerCase(); |
100 | | - if (normalized === 'passed') { |
101 | | - return 'background-color: var(--ds-background-success); color: var(--ds-text-success);'; |
102 | | - } |
103 | | - if (normalized === 'failed') { |
104 | | - return 'background-color: var(--ds-background-danger); color: var(--ds-text-danger);'; |
105 | | - } |
106 | | - if (normalized === 'blocked') { |
107 | | - return 'background-color: var(--ds-background-warning); color: var(--ds-text-warning);'; |
108 | | - } |
109 | | - if (normalized === 'in_progress') { |
110 | | - return 'background-color: var(--ds-background-information); color: var(--ds-text-information);'; |
111 | | - } |
112 | | - return 'background-color: var(--ds-background-neutral); color: var(--ds-text-subtle);'; |
| 106 | + if (normalized === 'passed') return 'green'; |
| 107 | + if (normalized === 'failed') return 'red'; |
| 108 | + if (normalized === 'blocked') return 'amber'; |
| 109 | + if (normalized === 'in_progress') return 'blue'; |
| 110 | + return 'gray'; |
113 | 111 | } |
114 | 112 |
|
115 | 113 | function getStatusIcon(status) { |
|
153 | 151 | return `${minutes}m`; |
154 | 152 | } |
155 | 153 |
|
156 | | - // Test case status style helper |
157 | | - function getTestCaseStatusStyle(status) { |
158 | | - if (status === 'inactive') { |
159 | | - return 'background-color: var(--ds-background-neutral); color: var(--ds-text-disabled);'; |
160 | | - } |
161 | | - if (status === 'draft') { |
162 | | - return 'background-color: var(--ds-background-warning-bold); color: white;'; |
163 | | - } |
164 | | - return 'background-color: var(--ds-background-success); color: var(--ds-text-success);'; |
| 154 | + function getTestCaseStatusColor(status) { |
| 155 | + if (status === 'inactive') return 'gray'; |
| 156 | + if (status === 'draft') return 'amber'; |
| 157 | + return 'green'; |
165 | 158 | } |
166 | 159 | </script> |
167 | 160 |
|
168 | 161 | {#if embedded} |
169 | | - <div class="w-full bg-white rounded-2xl shadow-2xl border border-gray-100 max-h-full flex flex-col overflow-hidden"> |
| 162 | + <Card variant="raised" padding="none" rounded="xl" shadow class="w-full max-h-full flex flex-col overflow-hidden"> |
170 | 163 | <div class="flex-1 overflow-y-auto"> |
171 | 164 | {@render previewContent()} |
172 | 165 | </div> |
173 | | - </div> |
| 166 | + </Card> |
174 | 167 | {:else} |
175 | 168 | <Modal |
176 | 169 | bind:isOpen |
|
184 | 177 |
|
185 | 178 | {#snippet previewContent()} |
186 | 179 | <div class="p-6 space-y-6"> |
187 | | - <div class="flex items-start justify-between gap-4"> |
188 | | - <div> |
189 | | - <p class="text-xs uppercase tracking-wide text-gray-400 mb-1">{t('testCase.preview')}</p> |
190 | | - <h2 class="text-2xl font-semibold" style="color: var(--ds-text);"> |
191 | | - {testCase ? testCase.title : t('common.loading')} |
192 | | - </h2> |
193 | | - {#if testCase?.folder_name} |
194 | | - <p class="text-sm mt-1" style="color: var(--ds-text-subtle);"> |
195 | | - {t('testCase.folder')}: {testCase.folder_name} |
196 | | - </p> |
| 180 | + <PageHeader |
| 181 | + title={testCase ? testCase.title : t('common.loading')} |
| 182 | + subtitle={testCase?.folder_name ? `${t('testCase.folder')}: ${testCase.folder_name}` : t('testCase.preview')} |
| 183 | + marginClass="mb-0" |
| 184 | + > |
| 185 | + {#snippet actions()} |
| 186 | + {#if embedded} |
| 187 | + <Button variant="ghost" icon={ArrowLeft} onclick={handleClose} dataTestid="test-case-detail-back"> |
| 188 | + {backLabel || t('testCase.backToItem')} |
| 189 | + </Button> |
197 | 190 | {/if} |
198 | | - <!-- Metadata badges --> |
199 | | - {#if testCase} |
200 | | - <div class="flex items-center gap-2 mt-3"> |
201 | | - <!-- Priority badge --> |
202 | | - <span |
203 | | - class="inline-flex items-center px-2 py-1 text-xs font-medium rounded text-white capitalize" |
204 | | - style="background-color: {getPriorityColor(testCase.priority || 'medium')};" |
205 | | - > |
206 | | - {testCase.priority || 'medium'} {t('testCase.priority')} |
207 | | - </span> |
208 | | - <!-- Status badge --> |
209 | | - <span |
210 | | - class="inline-flex items-center px-2 py-1 text-xs font-medium rounded capitalize" |
211 | | - style={getTestCaseStatusStyle(testCase.status || 'active')} |
212 | | - > |
213 | | - {testCase.status || 'active'} |
214 | | - </span> |
215 | | - <!-- Duration badge --> |
216 | | - {#if formatDuration(testCase.estimated_duration)} |
217 | | - <span |
218 | | - class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded" |
219 | | - style="background-color: var(--ds-background-neutral); color: var(--ds-text-subtle);" |
220 | | - > |
221 | | - <Clock class="w-3 h-3" /> |
222 | | - {formatDuration(testCase.estimated_duration)} |
223 | | - </span> |
224 | | - {/if} |
225 | | - </div> |
| 191 | + {/snippet} |
| 192 | + </PageHeader> |
| 193 | + {#if testCase} |
| 194 | + <div class="flex flex-wrap items-center gap-2"> |
| 195 | + <Lozenge customBg={getPriorityColor(testCase.priority || 'medium')} text={`${testCase.priority || 'medium'} ${t('testCase.priority')}`} /> |
| 196 | + <Lozenge color={getTestCaseStatusColor(testCase.status || 'active')} text={testCase.status || 'active'} /> |
| 197 | + {#if formatDuration(testCase.estimated_duration)} |
| 198 | + <Lozenge color="gray" icon={Clock} text={formatDuration(testCase.estimated_duration)} /> |
226 | 199 | {/if} |
227 | 200 | </div> |
228 | | - {#if embedded} |
229 | | - <button |
230 | | - class="inline-flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover-bg transition" |
231 | | - onclick={handleClose} |
232 | | - > |
233 | | - <ArrowLeft class="w-4 h-4" /> |
234 | | - {t('testCase.backToItem')} |
235 | | - </button> |
236 | | - {/if} |
237 | | - </div> |
| 201 | + {/if} |
238 | 202 |
|
239 | 203 | {#if loading} |
240 | | - <div class="flex items-center justify-center py-10"> |
241 | | - <div |
242 | | - class="h-12 w-12 animate-spin rounded-full border-2 border-t-transparent" |
243 | | - style="border-color: var(--ds-interactive); border-top-color: transparent;" |
244 | | - ></div> |
245 | | - </div> |
| 204 | + <StateDisplay type="loading" message={t('common.loading')} /> |
246 | 205 | {:else if error} |
247 | | - <div |
248 | | - class="rounded border p-4" |
249 | | - style="border-color: var(--ds-border-danger); background-color: var(--ds-background-danger); color: var(--ds-text-danger);" |
250 | | - > |
251 | | - {error} |
252 | | - </div> |
| 206 | + <StateDisplay type="error" title={t('common.error')} message={error} /> |
253 | 207 | {:else if testCase} |
254 | 208 | <div class="space-y-6"> |
255 | 209 | <!-- Action Buttons --> |
|
275 | 229 | </div> |
276 | 230 |
|
277 | 231 | {#if testCase.preconditions} |
278 | | - <div |
279 | | - class="rounded-2xl border shadow-sm p-5" |
280 | | - style="border-color: var(--ds-border); background-color: var(--ds-background-neutral); color: var(--ds-text);" |
281 | | - > |
282 | | - <p |
283 | | - class="text-xs font-semibold uppercase tracking-widest mb-2" |
284 | | - style="color: var(--ds-interactive);" |
285 | | - > |
286 | | - {t('testCase.preconditions')} |
287 | | - </p> |
288 | | - <p class="text-sm" style="color: var(--ds-text);"> |
289 | | - {testCase.preconditions} |
290 | | - </p> |
291 | | - </div> |
| 232 | + <AlertBox variant="info"> |
| 233 | + <strong>{t('testCase.preconditions')}:</strong> {testCase.preconditions} |
| 234 | + </AlertBox> |
292 | 235 | {/if} |
293 | 236 |
|
294 | 237 | <!-- Test Steps Section --> |
295 | | - <div |
296 | | - class="rounded-2xl border shadow-sm overflow-hidden" |
297 | | - style="border-color: var(--ds-border); background-color: var(--ds-surface-raised);" |
298 | | - > |
299 | | - <div class="flex items-center justify-between px-6 py-4 border-b" style="border-color: var(--ds-border);"> |
| 238 | + <Card variant="raised" padding="none" rounded="xl" shadow class="overflow-hidden"> |
| 239 | + {#snippet header()} |
300 | 240 | <h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--ds-text);"> |
301 | 241 | <ListOrdered class="w-5 h-5" style="color: var(--ds-interactive);" /> |
302 | 242 | {t('testCase.testSteps')} ({testSteps.length}) |
303 | 243 | </h2> |
304 | | - </div> |
| 244 | + {/snippet} |
305 | 245 | <div class="p-6"> |
306 | 246 | {#if testSteps.length === 0} |
307 | | - <div |
308 | | - class="rounded-xl border border-dashed text-center space-y-4 p-8" |
309 | | - style="border-color: var(--ds-border); background-color: var(--ds-background-neutral);" |
310 | | - > |
311 | | - <div |
312 | | - class="w-16 h-16 mx-auto flex items-center justify-center rounded-full" |
313 | | - style="background-color: var(--ds-accent-blue-subtler); color: var(--ds-interactive);" |
314 | | - > |
315 | | - <ClipboardList class="w-7 h-7" /> |
316 | | - </div> |
317 | | - <div> |
318 | | - <div class="text-lg font-medium mb-1" style="color: var(--ds-text);"> |
319 | | - {t('testCase.noStepsDefined')} |
320 | | - </div> |
321 | | - <div class="text-sm mb-4" style="color: var(--ds-text-subtle);"> |
322 | | - {t('testCase.noStepsHelp')} |
323 | | - </div> |
| 247 | + <EmptyState icon={ClipboardList} title={t('testCase.noStepsDefined')} description={t('testCase.noStepsHelp')}> |
| 248 | + {#snippet action()} |
324 | 249 | <Button |
325 | 250 | variant="primary" |
326 | 251 | icon={Edit} |
|
330 | 255 | > |
331 | 256 | {t('testCase.addSteps')} |
332 | 257 | </Button> |
333 | | - </div> |
334 | | - </div> |
| 258 | + {/snippet} |
| 259 | + </EmptyState> |
335 | 260 | {:else} |
336 | 261 | <div class="space-y-4"> |
337 | 262 | {#each testSteps as step} |
338 | | - <div |
339 | | - class="rounded-xl border shadow-sm p-4" |
340 | | - style="border-color: var(--ds-border); background-color: var(--ds-surface);" |
341 | | - > |
| 263 | + <Panel padding="default" rounded="lg" style="background-color: var(--ds-surface);"> |
342 | 264 | <div class="flex items-start gap-4"> |
343 | 265 | <div |
344 | 266 | class="flex-shrink-0 w-10 h-10 rounded-full text-white font-semibold text-base flex items-center justify-center" |
|
383 | 305 | </div> |
384 | 306 | </div> |
385 | 307 | </div> |
386 | | - </div> |
| 308 | + </Panel> |
387 | 309 | {/each} |
388 | 310 | </div> |
389 | 311 | {/if} |
390 | 312 | </div> |
391 | | - </div> |
| 313 | + </Card> |
392 | 314 |
|
393 | 315 | <!-- Recent Executions Section --> |
394 | | - <div |
395 | | - class="rounded-2xl border shadow-sm overflow-hidden" |
396 | | - style="border-color: var(--ds-border); background-color: var(--ds-surface-raised);" |
397 | | - > |
398 | | - <div class="flex items-center justify-between px-6 py-4 border-b" style="border-color: var(--ds-border);"> |
| 316 | + <Card variant="raised" padding="none" rounded="xl" shadow class="overflow-hidden"> |
| 317 | + {#snippet header()} |
399 | 318 | <h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--ds-text);"> |
400 | 319 | <Play class="w-5 h-5" style="color: var(--ds-icon-accent-green);" /> |
401 | 320 | {t('testCase.recentExecutions')} ({executions.length}) |
402 | 321 | </h2> |
403 | | - </div> |
| 322 | + {/snippet} |
404 | 323 | <div class="p-6"> |
405 | 324 | {#if executions.length === 0} |
406 | | - <div |
407 | | - class="rounded-xl border border-dashed text-center space-y-3 p-6" |
408 | | - style="border-color: var(--ds-border); background-color: var(--ds-background-neutral);" |
409 | | - > |
410 | | - <div |
411 | | - class="w-14 h-14 mx-auto flex items-center justify-center rounded-full" |
412 | | - style="background-color: var(--ds-accent-blue-subtler); color: var(--ds-text-information);" |
413 | | - > |
414 | | - <History class="w-6 h-6" /> |
415 | | - </div> |
416 | | - <div class="text-sm" style="color: var(--ds-text-subtle);"> |
417 | | - {t('testCase.noExecutions')} |
418 | | - </div> |
419 | | - </div> |
| 325 | + <EmptyState icon={History} title={t('testCase.noExecutions')} /> |
420 | 326 | {:else} |
421 | 327 | <div class="space-y-3"> |
422 | 328 | {#each executions as execution} |
423 | 329 | {@const StatusIcon = getStatusIcon(execution.status)} |
424 | | - <a |
| 330 | + <Panel |
425 | 331 | href={`${workspaceTestsBasePath}/runs/${execution.run_id}`} |
426 | | - class="flex items-start gap-3 rounded-xl border p-4 transition hover:shadow-sm" |
427 | 332 | style="border-color: var(--ds-border); background-color: var(--ds-surface);" |
| 333 | + padding="default" |
| 334 | + rounded="lg" |
| 335 | + interactive |
428 | 336 | > |
429 | | - <div class="flex-shrink-0 mt-0.5"> |
| 337 | + <div class="flex items-start gap-3"> |
| 338 | + <div class="flex-shrink-0 mt-0.5"> |
430 | 339 | <StatusIcon |
431 | 340 | class="w-5 h-5" |
432 | 341 | style={`color: ${getStatusIconColor(execution.status)};`} |
433 | 342 | /> |
434 | | - </div> |
435 | | - <div class="flex-1 min-w-0"> |
| 343 | + </div> |
| 344 | + <div class="flex-1 min-w-0"> |
436 | 345 | <div class="flex items-center justify-between gap-3 mb-1"> |
437 | 346 | <div class="font-semibold text-sm truncate" style="color: var(--ds-text);"> |
438 | 347 | {execution.run_name} |
439 | 348 | </div> |
440 | | - <span |
441 | | - class="text-xs px-2 py-1 rounded-full font-semibold whitespace-nowrap" |
442 | | - style={getStatusPillStyle(execution.status)} |
443 | | - > |
444 | | - {execution.status || 'not_run'} |
445 | | - </span> |
| 349 | + <Lozenge color={getStatusColor(execution.status)} text={execution.status || 'not_run'} /> |
446 | 350 | </div> |
447 | 351 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs" style="color: var(--ds-text-subtle);"> |
448 | 352 | <span class="flex items-center gap-1"> |
|
456 | 360 | <span>• {t('testCase.template')}: {execution.template_name}</span> |
457 | 361 | {/if} |
458 | 362 | </div> |
| 363 | + </div> |
459 | 364 | </div> |
460 | | - </a> |
| 365 | + </Panel> |
461 | 366 | {/each} |
462 | 367 | </div> |
463 | 368 | {/if} |
464 | 369 | </div> |
465 | | - </div> |
| 370 | + </Card> |
466 | 371 | </div> |
467 | 372 | {/if} |
468 | 373 | </div> |
|
0 commit comments