Skip to content

Commit e175c8c

Browse files
committed
Modernize test management frontend
1 parent 49cada5 commit e175c8c

16 files changed

Lines changed: 658 additions & 993 deletions

frontend/src/lib/components/StatCard.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
label,
1212
value,
1313
color = 'blue', // 'blue', 'green', 'orange', 'purple'
14-
href = null
14+
href = null,
15+
dataTestid = undefined
1516
} = $props();
1617
1718
// Gradient background colors (subtle tint towards the accent color)
@@ -53,7 +54,7 @@
5354
<div class="ml-3 w-0 flex-1">
5455
<dl>
5556
<dt class="text-xs font-medium truncate" style="color: var(--ds-text-subtle);">{label}</dt>
56-
<dd class="text-xl font-semibold truncate" style="color: var(--ds-text);" title={typeof value === 'string' ? value : undefined}>{value}</dd>
57+
<dd data-testid={dataTestid} class="text-xl font-semibold truncate" style="color: var(--ds-text);" title={typeof value === 'string' ? value : undefined}>{value}</dd>
5758
</dl>
5859
</div>
5960
</div>

frontend/src/lib/dialogs/TestCaseViewModal.svelte

Lines changed: 71 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
} from '@lucide/svelte';
1414
import Modal from './Modal.svelte';
1515
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';
1623
import { api } from '../api.js';
1724
import { formatAuthenticatedDateTime as formatDateTimeLocale } from '../utils/authenticatedDateFormatter.js';
1825
import { t } from '../stores/i18n.svelte.js';
@@ -22,6 +29,7 @@
2229
testCaseId = null,
2330
workspaceId = null,
2431
embedded = false,
32+
backLabel = null,
2533
onclose = null
2634
} = $props();
2735
@@ -92,24 +100,14 @@
92100
onclose?.();
93101
}
94102
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';
99105
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';
113111
}
114112
115113
function getStatusIcon(status) {
@@ -153,24 +151,19 @@
153151
return `${minutes}m`;
154152
}
155153
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';
165158
}
166159
</script>
167160
168161
{#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">
170163
<div class="flex-1 overflow-y-auto">
171164
{@render previewContent()}
172165
</div>
173-
</div>
166+
</Card>
174167
{:else}
175168
<Modal
176169
bind:isOpen
@@ -184,72 +177,33 @@
184177
185178
{#snippet previewContent()}
186179
<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>
197190
{/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)} />
226199
{/if}
227200
</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}
238202
239203
{#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')} />
246205
{: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} />
253207
{:else if testCase}
254208
<div class="space-y-6">
255209
<!-- Action Buttons -->
@@ -275,52 +229,23 @@
275229
</div>
276230
277231
{#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>
292235
{/if}
293236
294237
<!-- 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()}
300240
<h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--ds-text);">
301241
<ListOrdered class="w-5 h-5" style="color: var(--ds-interactive);" />
302242
{t('testCase.testSteps')} ({testSteps.length})
303243
</h2>
304-
</div>
244+
{/snippet}
305245
<div class="p-6">
306246
{#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()}
324249
<Button
325250
variant="primary"
326251
icon={Edit}
@@ -330,15 +255,12 @@
330255
>
331256
{t('testCase.addSteps')}
332257
</Button>
333-
</div>
334-
</div>
258+
{/snippet}
259+
</EmptyState>
335260
{:else}
336261
<div class="space-y-4">
337262
{#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);">
342264
<div class="flex items-start gap-4">
343265
<div
344266
class="flex-shrink-0 w-10 h-10 rounded-full text-white font-semibold text-base flex items-center justify-center"
@@ -383,66 +305,48 @@
383305
</div>
384306
</div>
385307
</div>
386-
</div>
308+
</Panel>
387309
{/each}
388310
</div>
389311
{/if}
390312
</div>
391-
</div>
313+
</Card>
392314
393315
<!-- 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()}
399318
<h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--ds-text);">
400319
<Play class="w-5 h-5" style="color: var(--ds-icon-accent-green);" />
401320
{t('testCase.recentExecutions')} ({executions.length})
402321
</h2>
403-
</div>
322+
{/snippet}
404323
<div class="p-6">
405324
{#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')} />
420326
{:else}
421327
<div class="space-y-3">
422328
{#each executions as execution}
423329
{@const StatusIcon = getStatusIcon(execution.status)}
424-
<a
330+
<Panel
425331
href={`${workspaceTestsBasePath}/runs/${execution.run_id}`}
426-
class="flex items-start gap-3 rounded-xl border p-4 transition hover:shadow-sm"
427332
style="border-color: var(--ds-border); background-color: var(--ds-surface);"
333+
padding="default"
334+
rounded="lg"
335+
interactive
428336
>
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">
430339
<StatusIcon
431340
class="w-5 h-5"
432341
style={`color: ${getStatusIconColor(execution.status)};`}
433342
/>
434-
</div>
435-
<div class="flex-1 min-w-0">
343+
</div>
344+
<div class="flex-1 min-w-0">
436345
<div class="flex items-center justify-between gap-3 mb-1">
437346
<div class="font-semibold text-sm truncate" style="color: var(--ds-text);">
438347
{execution.run_name}
439348
</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'} />
446350
</div>
447351
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs" style="color: var(--ds-text-subtle);">
448352
<span class="flex items-center gap-1">
@@ -456,13 +360,14 @@
456360
<span>• {t('testCase.template')}: {execution.template_name}</span>
457361
{/if}
458362
</div>
363+
</div>
459364
</div>
460-
</a>
365+
</Panel>
461366
{/each}
462367
</div>
463368
{/if}
464369
</div>
465-
</div>
370+
</Card>
466371
</div>
467372
{/if}
468373
</div>

0 commit comments

Comments
 (0)