Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 11s
Tonemark is a SvelteKit PWA for transcribing YouTube videos, audio and video files, and microphone recordings using a local Whisper backend. Features: - Dark glassmorphic UI with electric-lime accent (5 switchable themes) - Rail nav (desktop) / tab bar (mobile) layout - Drop zone, YouTube URL input, and live audio recording inputs - Audio mode waveform cards (none / standard / aggressive / auto) - Real-time transcription progress with animated waveform - Job queue with SSE streaming updates - Push notifications on job completion - PWA with native SvelteKit service worker - SRT / TXT / MD / JSON transcript downloads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { vi } from 'vitest';
|
|
import { mkdtemp, rm } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
// Set DATA_DIR before db module is imported (module-level DB init)
|
|
const TEST_DATA_DIR = join(tmpdir(), 'whisper-pwa-db-test-' + Date.now());
|
|
vi.stubEnv('DATA_DIR', TEST_DATA_DIR);
|
|
|
|
import {
|
|
createJob,
|
|
getJob,
|
|
listJobs,
|
|
updateJob,
|
|
setJobStatus,
|
|
savePushSubscription,
|
|
getAllSubscriptions,
|
|
deletePushSubscription
|
|
} from '$lib/server/db.js';
|
|
|
|
afterAll(async () => {
|
|
await rm(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
// ── createJob / getJob ────────────────────────────────────────────────────────
|
|
|
|
describe('createJob', () => {
|
|
it('creates a job and returns it with a UUID', () => {
|
|
const job = createJob('https://youtu.be/abc', 'My Video', 'auto');
|
|
expect(job.id).toMatch(/^[0-9a-f-]{36}$/);
|
|
expect(job.source).toBe('https://youtu.be/abc');
|
|
expect(job.title).toBe('My Video');
|
|
expect(job.audioMode).toBe('auto');
|
|
});
|
|
|
|
it('defaults status to pending', () => {
|
|
const job = createJob('src', 'title', 'standard');
|
|
expect(job.status).toBe('pending');
|
|
});
|
|
|
|
it('defaults progress to 0', () => {
|
|
const job = createJob('src', 'title', 'none');
|
|
expect(job.progress).toBe(0);
|
|
});
|
|
|
|
it('assigns unique IDs to each job', () => {
|
|
const a = createJob('src', 'A', 'auto');
|
|
const b = createJob('src', 'B', 'auto');
|
|
expect(a.id).not.toBe(b.id);
|
|
});
|
|
});
|
|
|
|
describe('getJob', () => {
|
|
it('returns null for an unknown id', () => {
|
|
expect(getJob('00000000-0000-0000-0000-000000000000')).toBeNull();
|
|
});
|
|
|
|
it('returns the correct job by id', () => {
|
|
const created = createJob('https://youtu.be/xyz', 'Find Me', 'aggressive');
|
|
const found = getJob(created.id);
|
|
expect(found).not.toBeNull();
|
|
expect(found!.id).toBe(created.id);
|
|
expect(found!.title).toBe('Find Me');
|
|
});
|
|
});
|
|
|
|
// ── updateJob ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('updateJob', () => {
|
|
it('updates only specified fields, preserving others', () => {
|
|
const job = createJob('src', 'Original Title', 'auto');
|
|
updateJob({ id: job.id, title: 'New Title' });
|
|
const updated = getJob(job.id)!;
|
|
expect(updated.title).toBe('New Title');
|
|
expect(updated.source).toBe('src'); // unchanged
|
|
expect(updated.audioMode).toBe('auto'); // unchanged
|
|
});
|
|
|
|
it('stores whisperJobId', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
updateJob({ id: job.id, whisperJobId: 'whisper-uuid-123' });
|
|
expect(getJob(job.id)!.whisperJobId).toBe('whisper-uuid-123');
|
|
});
|
|
|
|
it('stores meanVolume', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
updateJob({ id: job.id, meanVolume: -20.5 });
|
|
expect(getJob(job.id)!.meanVolume).toBe(-20.5);
|
|
});
|
|
|
|
it('stores segmentsJson', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
const json = JSON.stringify([{ index: 0, start: 0, end: 5, text: 'Hello', words: [] }]);
|
|
updateJob({ id: job.id, segmentsJson: json });
|
|
expect(getJob(job.id)!.segmentsJson).toBe(json);
|
|
});
|
|
|
|
it('stores error message', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
updateJob({ id: job.id, status: 'failed', error: 'something went wrong' });
|
|
const updated = getJob(job.id)!;
|
|
expect(updated.status).toBe('failed');
|
|
expect(updated.error).toBe('something went wrong');
|
|
});
|
|
|
|
it('does nothing for an unknown id', () => {
|
|
expect(() => updateJob({ id: 'no-such-id', title: 'Ghost' })).not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ── setJobStatus ──────────────────────────────────────────────────────────────
|
|
|
|
describe('setJobStatus', () => {
|
|
it('updates status without touching other fields', () => {
|
|
const job = createJob('src', 'Status Test', 'auto');
|
|
setJobStatus(job.id, 'downloading', 5);
|
|
const updated = getJob(job.id)!;
|
|
expect(updated.status).toBe('downloading');
|
|
expect(updated.progress).toBe(5);
|
|
expect(updated.title).toBe('Status Test'); // unchanged
|
|
});
|
|
|
|
it('transitions through all valid statuses', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
const statuses = ['downloading', 'preparing', 'transcribing', 'processing', 'done'] as const;
|
|
for (const status of statuses) {
|
|
setJobStatus(job.id, status, 50);
|
|
expect(getJob(job.id)!.status).toBe(status);
|
|
}
|
|
});
|
|
|
|
it('defaults progress to 0 if not supplied', () => {
|
|
const job = createJob('src', 'title', 'auto');
|
|
setJobStatus(job.id, 'preparing');
|
|
expect(getJob(job.id)!.progress).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ── listJobs ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('listJobs', () => {
|
|
it('returns jobs in descending creation order', () => {
|
|
const a = createJob('src', 'Alpha', 'auto');
|
|
const b = createJob('src', 'Beta', 'auto');
|
|
const jobs = listJobs();
|
|
const ids = jobs.map((j) => j.id);
|
|
expect(ids.indexOf(b.id)).toBeLessThan(ids.indexOf(a.id));
|
|
});
|
|
|
|
it('returns an array (possibly empty)', () => {
|
|
expect(Array.isArray(listJobs())).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── Push subscriptions ────────────────────────────────────────────────────────
|
|
|
|
describe('push subscriptions', () => {
|
|
const sub1 = { endpoint: 'https://push.example.com/abc', p256dh: 'p256dh-value-1', auth: 'auth-1' };
|
|
const sub2 = { endpoint: 'https://push.example.com/xyz', p256dh: 'p256dh-value-2', auth: 'auth-2' };
|
|
|
|
it('saves and retrieves a subscription', () => {
|
|
savePushSubscription(sub1);
|
|
const subs = getAllSubscriptions();
|
|
const found = subs.find((s) => s.endpoint === sub1.endpoint);
|
|
expect(found).toBeDefined();
|
|
expect(found!.p256dh).toBe(sub1.p256dh);
|
|
expect(found!.auth).toBe(sub1.auth);
|
|
});
|
|
|
|
it('upserts on duplicate endpoint (updates keys)', () => {
|
|
savePushSubscription(sub1);
|
|
const updated = { ...sub1, p256dh: 'new-p256dh', auth: 'new-auth' };
|
|
savePushSubscription(updated);
|
|
const subs = getAllSubscriptions().filter((s) => s.endpoint === sub1.endpoint);
|
|
expect(subs).toHaveLength(1);
|
|
expect(subs[0].p256dh).toBe('new-p256dh');
|
|
});
|
|
|
|
it('deletes by endpoint', () => {
|
|
savePushSubscription(sub2);
|
|
deletePushSubscription(sub2.endpoint);
|
|
const found = getAllSubscriptions().find((s) => s.endpoint === sub2.endpoint);
|
|
expect(found).toBeUndefined();
|
|
});
|
|
|
|
it('stores multiple subscriptions independently', () => {
|
|
savePushSubscription(sub1);
|
|
savePushSubscription(sub2);
|
|
const subs = getAllSubscriptions();
|
|
const endpoints = subs.map((s) => s.endpoint);
|
|
expect(endpoints).toContain(sub1.endpoint);
|
|
expect(endpoints).toContain(sub2.endpoint);
|
|
});
|
|
});
|