diff --git a/.gitignore b/.gitignore index 4d60acc..467d917 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ Thumbs.db tmp-test-remux/ tmp-test-*/ +# Coverage output +coverage/ + # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/src/lib/components/JobErrorLine.svelte b/src/lib/components/JobErrorLine.svelte index e78764f..fb5af18 100644 --- a/src/lib/components/JobErrorLine.svelte +++ b/src/lib/components/JobErrorLine.svelte @@ -3,155 +3,24 @@ interface Props { job: Job; - accentColor: string; - onRetry: (jobId: string) => Promise; - onDelete?: (jobId: string) => Promise; } - 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(); -
- - - {errorText} - -
- - - {#if onDelete && !retrying} - - {/if} -
-
+{#if job.status === 'failed' && job.error} +
{job.error}
+{/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f8e65fd..15fb8a7 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -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; @@ -282,9 +271,7 @@
{job.title || job.source}
{jobMeta(job)}
- {#if job.status === 'failed' && job.error} - - {/if} +
{#if job.status !== 'done' && job.status !== 'failed' && job.status !== 'cancelled'}
diff --git a/src/tests/__mocks__/app-environment.ts b/src/tests/__mocks__/app-environment.ts new file mode 100644 index 0000000..e77f65c --- /dev/null +++ b/src/tests/__mocks__/app-environment.ts @@ -0,0 +1,4 @@ +export const browser = true; +export const building = false; +export const dev = false; +export const version = 'test'; diff --git a/src/tests/job-list-error-display.test.ts b/src/tests/job-list-error-display.test.ts new file mode 100644 index 0000000..dc3f369 --- /dev/null +++ b/src/tests/job-list-error-display.test.ts @@ -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 { + 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}"`); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index dd84ad8..f5198f1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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'), } } });