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-remux/
|
||||||
tmp-test-*/
|
tmp-test-*/
|
||||||
|
|
||||||
|
# Coverage output
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|||||||
@@ -3,155 +3,24 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
job: Job;
|
job: Job;
|
||||||
accentColor: string;
|
|
||||||
onRetry: (jobId: string) => Promise<void>;
|
|
||||||
onDelete?: (jobId: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { job, accentColor, onRetry, onDelete }: Props = $props();
|
let { job }: 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);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if job.status === 'failed' && job.error}
|
||||||
class="job-error-line"
|
<div class="recent-err-line" title={job.error}>{job.error}</div>
|
||||||
role={isFailed ? 'alert' : 'status'}
|
{/if}
|
||||||
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}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.job-error-line {
|
.recent-err-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;
|
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
color: var(--error-text, #ff8a8a);
|
color: var(--error-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-family: var(--font-mono, 'JetBrains Mono', 'SF Mono', ui-monospace, monospace);
|
margin-top: 3px;
|
||||||
}
|
font-family: var(--font-mono);
|
||||||
|
max-width: 100%;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-14
@@ -101,17 +101,6 @@
|
|||||||
return parts.join(' · ');
|
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)
|
// Decorative waveform bars for the drop zone (80 bars)
|
||||||
const DROPZONE_BARS = 80;
|
const DROPZONE_BARS = 80;
|
||||||
</script>
|
</script>
|
||||||
@@ -282,9 +271,7 @@
|
|||||||
<div class="recent-text">
|
<div class="recent-text">
|
||||||
<div class="recent-title">{job.title || job.source}</div>
|
<div class="recent-title">{job.title || job.source}</div>
|
||||||
<div class="recent-meta mono">{jobMeta(job)}</div>
|
<div class="recent-meta mono">{jobMeta(job)}</div>
|
||||||
{#if job.status === 'failed' && job.error}
|
<JobErrorLine {job} />
|
||||||
<JobErrorLine {job} accentColor={ACCENT} onRetry={handleRetry} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if job.status !== 'done' && job.status !== 'failed' && job.status !== 'cancelled'}
|
{#if job.status !== 'done' && job.status !== 'failed' && job.status !== 'cancelled'}
|
||||||
<div class="recent-progress mono" style="color: {ACCENT}">
|
<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';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte({ hot: false })],
|
plugins: [svelte()],
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
globals: true,
|
globals: true,
|
||||||
fileParallelism: false,
|
fileParallelism: false,
|
||||||
include: ['src/tests/**/*.test.ts'],
|
include: ['src/tests/**/*.test.ts'],
|
||||||
environmentMatchGlobs: [
|
|
||||||
['**/job-error-line.test.ts', 'happy-dom']
|
|
||||||
],
|
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'html'],
|
reporter: ['text', 'html'],
|
||||||
@@ -19,9 +16,9 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
conditions: ['browser'],
|
|
||||||
alias: {
|
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