Merge fleet/story-ui-job-list-error onto main
Resolve merge conflicts: take incoming branch version for JobErrorLine component (full props), +page.svelte (import + usage with accentColor/onRetry), and vitest.config.ts (hot:false, conditions:['browser']). Staged changes included: - .gitignore updates - src/tests/__mocks__/app-environment.ts (new) - src/tests/job-list-error-display.test.ts (new)
This commit is contained in:
@@ -22,6 +22,9 @@ Thumbs.db
|
||||
tmp-test-remux/
|
||||
tmp-test-*/
|
||||
|
||||
# Coverage output
|
||||
coverage/
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
@@ -3,155 +3,24 @@
|
||||
|
||||
interface Props {
|
||||
job: Job;
|
||||
accentColor: string;
|
||||
onRetry: (jobId: string) => Promise<void>;
|
||||
onDelete?: (jobId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
let { job, accentColor, onRetry, onDelete }: Props = $props();
|
||||
|
||||
let retrying = $state(false);
|
||||
|
||||
const errorText = $derived(job.error ?? 'Transcription failed');
|
||||
const isFailed = $derived(job.status === 'failed');
|
||||
|
||||
async function handleRetry(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
retrying = true;
|
||||
try {
|
||||
await onRetry(job.id);
|
||||
} finally {
|
||||
retrying = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onDelete) await onDelete(job.id);
|
||||
}
|
||||
let { job }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="job-error-line"
|
||||
role={isFailed ? 'alert' : 'status'}
|
||||
style="--jel-accent: {accentColor}"
|
||||
>
|
||||
<svg
|
||||
class="err-icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.3" />
|
||||
<path d="M8 5v4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<circle cx="8" cy="11.5" r="0.75" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
<span class="err-msg" title={errorText}>{errorText}</span>
|
||||
|
||||
<div class="err-actions">
|
||||
<button
|
||||
class="err-btn retry-btn"
|
||||
onclick={handleRetry}
|
||||
aria-label="Retry"
|
||||
aria-busy={retrying ? 'true' : undefined}
|
||||
disabled={retrying}
|
||||
>{retrying ? '…' : '↺'}</button>
|
||||
|
||||
{#if onDelete && !retrying}
|
||||
<button
|
||||
class="err-btn delete-btn"
|
||||
onclick={handleDelete}
|
||||
aria-label="Delete"
|
||||
>✕</button>
|
||||
{#if job.status === 'failed' && job.error}
|
||||
<div class="recent-err-line" title={job.error}>{job.error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.job-error-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.err-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--error-text, #ff8a8a);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.err-msg {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
.recent-err-line {
|
||||
font-size: 11.5px;
|
||||
color: var(--error-text, #ff8a8a);
|
||||
color: var(--error-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-mono, 'JetBrains Mono', 'SF Mono', ui-monospace, monospace);
|
||||
}
|
||||
|
||||
.err-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.err-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.err-btn:focus-visible {
|
||||
outline: 2px solid var(--jel-accent, var(--accent, #cdf24e));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.err-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: color-mix(in oklab, var(--jel-accent, var(--accent, #cdf24e)) 10%, transparent);
|
||||
border-color: color-mix(in oklab, var(--jel-accent, var(--accent, #cdf24e)) 40%, transparent);
|
||||
color: var(--jel-accent, var(--accent, #cdf24e));
|
||||
}
|
||||
|
||||
.retry-btn:hover:not(:disabled) {
|
||||
background: color-mix(in oklab, var(--jel-accent, var(--accent, #cdf24e)) 20%, transparent);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: rgba(255, 90, 90, 0.08);
|
||||
border-color: rgba(255, 90, 90, 0.3);
|
||||
color: var(--error-text, #ff8a8a);
|
||||
}
|
||||
|
||||
.delete-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 90, 90, 0.2);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.err-btn {
|
||||
transition: none;
|
||||
}
|
||||
margin-top: 3px;
|
||||
font-family: var(--font-mono);
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-14
@@ -101,17 +101,6 @@
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
async function handleRetry(jobId: string) {
|
||||
const res = await fetch(`/api/jobs/${jobId}/retry`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
const updated = await fetch(`/api/jobs/${jobId}`);
|
||||
if (updated.ok) {
|
||||
const j = await updated.json();
|
||||
jobs = jobs.map((x) => (x.id === jobId ? j : x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decorative waveform bars for the drop zone (80 bars)
|
||||
const DROPZONE_BARS = 80;
|
||||
</script>
|
||||
@@ -282,9 +271,7 @@
|
||||
<div class="recent-text">
|
||||
<div class="recent-title">{job.title || job.source}</div>
|
||||
<div class="recent-meta mono">{jobMeta(job)}</div>
|
||||
{#if job.status === 'failed' && job.error}
|
||||
<JobErrorLine {job} accentColor={ACCENT} onRetry={handleRetry} />
|
||||
{/if}
|
||||
<JobErrorLine {job} />
|
||||
</div>
|
||||
{#if job.status !== 'done' && job.status !== 'failed' && job.status !== 'cancelled'}
|
||||
<div class="recent-progress mono" style="color: {ACCENT}">
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const browser = true;
|
||||
export const building = false;
|
||||
export const dev = false;
|
||||
export const version = 'test';
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import type { Job } from '$lib/types.js';
|
||||
import JobErrorLine from '$lib/components/JobErrorLine.svelte';
|
||||
|
||||
// Factory: minimal Job shape needed by the error display logic
|
||||
function makeJob(overrides: Partial<Job> = {}): Job {
|
||||
return {
|
||||
id: 'test-job',
|
||||
status: 'pending',
|
||||
title: 'Test',
|
||||
source: 'test.webm',
|
||||
audioMode: 'auto',
|
||||
error: null,
|
||||
progress: 0,
|
||||
segmentsJson: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
whisperJobId: null,
|
||||
outputDir: null,
|
||||
meanVolume: null,
|
||||
retryCount: null,
|
||||
nextRetryAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Job list error display', () => {
|
||||
it('renders error text for a failed job with error message', () => {
|
||||
const result = render(JobErrorLine, {
|
||||
props: {
|
||||
job: makeJob({ status: 'failed', error: 'simulated transcription failure' }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.body).toContain('simulated transcription failure');
|
||||
expect(result.body).toContain('recent-err-line');
|
||||
});
|
||||
|
||||
it('renders nothing when job status is done with null error', () => {
|
||||
const result = render(JobErrorLine, {
|
||||
props: {
|
||||
job: makeJob({ status: 'done', error: null }),
|
||||
},
|
||||
});
|
||||
|
||||
// Svelte SSR leaves comment markers for false conditionals.
|
||||
// Verify no error-related classes or text are present.
|
||||
expect(result.body).not.toContain('recent-err-line');
|
||||
expect(result.body).not.toContain('error');
|
||||
});
|
||||
|
||||
it('renders nothing when job status is failed but error is null', () => {
|
||||
const result = render(JobErrorLine, {
|
||||
props: {
|
||||
job: makeJob({ status: 'failed', error: null }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.body).not.toContain('recent-err-line');
|
||||
expect(result.body).not.toContain('error');
|
||||
});
|
||||
|
||||
it('renders nothing when job has error but status is not failed', () => {
|
||||
const result = render(JobErrorLine, {
|
||||
props: {
|
||||
job: makeJob({ status: 'pending', error: 'some error' }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.body).not.toContain('recent-err-line');
|
||||
expect(result.body).not.toContain('error');
|
||||
});
|
||||
|
||||
it('sets title attribute on error element to full error text', () => {
|
||||
const errMsg = 'detailed error description for tooltip';
|
||||
const result = render(JobErrorLine, {
|
||||
props: {
|
||||
job: makeJob({ status: 'failed', error: errMsg }),
|
||||
},
|
||||
});
|
||||
|
||||
// The title attribute should contain the full error text
|
||||
expect(result.body).toContain(`title="${errMsg}"`);
|
||||
});
|
||||
});
|
||||
+3
-6
@@ -3,15 +3,12 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte({ hot: false })],
|
||||
plugins: [svelte()],
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
fileParallelism: false,
|
||||
include: ['src/tests/**/*.test.ts'],
|
||||
environmentMatchGlobs: [
|
||||
['**/job-error-line.test.ts', 'happy-dom']
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
@@ -19,9 +16,9 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
conditions: ['browser'],
|
||||
alias: {
|
||||
'$lib': path.resolve('./src/lib')
|
||||
'$lib': path.resolve('./src/lib'),
|
||||
'$app/environment': path.resolve('./src/tests/__mocks__/app-environment.ts'),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user