mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 10:54:00 -06:00
Full-featured self-hosted yt-dlp web frontend:
- Python 3.12+ / FastAPI backend with async SQLite, SSE transport, session isolation
- Vue 3 / TypeScript / Pinia frontend with real-time progress, theme picker
- 3 built-in themes (cyberpunk/dark/light) + drop-in custom theme system
- Admin auth (bcrypt), purge system, cookie upload, file serving
- Docker multi-stage build, GitHub Actions CI/CD
- 179 backend tests, 29 frontend tests (208 total)
Slices: S01 (Foundation), S02 (SSE+Sessions), S03 (Frontend),
S04 (Admin+Auth), S05 (Themes), S06 (Docker+CI)
198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
import { useDownloadsStore } from '@/stores/downloads'
|
|
import type { Job, ProgressEvent } from '@/api/types'
|
|
|
|
function makeJob(overrides: Partial<Job> = {}): Job {
|
|
return {
|
|
id: overrides.id ?? 'job-1',
|
|
session_id: 'sess-1',
|
|
url: 'https://example.com/video',
|
|
status: 'queued',
|
|
format_id: null,
|
|
quality: null,
|
|
output_template: null,
|
|
filename: null,
|
|
filesize: null,
|
|
progress_percent: 0,
|
|
speed: null,
|
|
eta: null,
|
|
error_message: null,
|
|
created_at: '2026-03-18T00:00:00Z',
|
|
started_at: null,
|
|
completed_at: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('downloads store', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
describe('handleInit', () => {
|
|
it('populates jobs from init event', () => {
|
|
const store = useDownloadsStore()
|
|
const jobs = [makeJob({ id: 'a' }), makeJob({ id: 'b' })]
|
|
store.handleInit(jobs)
|
|
|
|
expect(store.jobs.size).toBe(2)
|
|
expect(store.jobs.get('a')).toBeDefined()
|
|
expect(store.jobs.get('b')).toBeDefined()
|
|
})
|
|
|
|
it('merges with existing jobs on re-init (avoids race with local submits)', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([makeJob({ id: 'old' })])
|
|
expect(store.jobs.has('old')).toBe(true)
|
|
|
|
store.handleInit([makeJob({ id: 'new' })])
|
|
// Merge keeps both old (locally submitted) and new (SSE replay)
|
|
expect(store.jobs.has('old')).toBe(true)
|
|
expect(store.jobs.has('new')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('handleJobUpdate', () => {
|
|
it('updates progress on existing job', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([makeJob({ id: 'j1' })])
|
|
|
|
const event: ProgressEvent = {
|
|
job_id: 'j1',
|
|
status: 'downloading',
|
|
percent: 45.5,
|
|
speed: '2.5 MiB/s',
|
|
eta: '1m30s',
|
|
downloaded_bytes: null,
|
|
total_bytes: null,
|
|
filename: 'video.mp4',
|
|
}
|
|
store.handleJobUpdate(event)
|
|
|
|
const job = store.jobs.get('j1')!
|
|
expect(job.status).toBe('downloading')
|
|
expect(job.progress_percent).toBe(45.5)
|
|
expect(job.speed).toBe('2.5 MiB/s')
|
|
expect(job.eta).toBe('1m30s')
|
|
expect(job.filename).toBe('video.mp4')
|
|
})
|
|
|
|
it('normalizes yt-dlp "finished" status to "completed"', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([makeJob({ id: 'j1' })])
|
|
|
|
store.handleJobUpdate({
|
|
job_id: 'j1',
|
|
status: 'finished',
|
|
percent: 100,
|
|
speed: null,
|
|
eta: null,
|
|
downloaded_bytes: null,
|
|
total_bytes: null,
|
|
filename: 'video.mp4',
|
|
})
|
|
|
|
expect(store.jobs.get('j1')!.status).toBe('completed')
|
|
})
|
|
|
|
it('creates minimal entry for unknown job (cross-tab scenario)', () => {
|
|
const store = useDownloadsStore()
|
|
const event: ProgressEvent = {
|
|
job_id: 'nonexistent',
|
|
status: 'downloading',
|
|
percent: 50,
|
|
speed: null,
|
|
eta: null,
|
|
downloaded_bytes: null,
|
|
total_bytes: null,
|
|
filename: null,
|
|
}
|
|
// Should not throw — creates a minimal placeholder entry
|
|
store.handleJobUpdate(event)
|
|
expect(store.jobs.size).toBe(1)
|
|
expect(store.jobs.get('nonexistent')!.status).toBe('downloading')
|
|
})
|
|
})
|
|
|
|
describe('handleJobRemoved', () => {
|
|
it('removes job from map', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([makeJob({ id: 'j1' }), makeJob({ id: 'j2' })])
|
|
|
|
store.handleJobRemoved('j1')
|
|
expect(store.jobs.has('j1')).toBe(false)
|
|
expect(store.jobs.has('j2')).toBe(true)
|
|
})
|
|
|
|
it('no-ops for unknown job', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([makeJob({ id: 'j1' })])
|
|
store.handleJobRemoved('nonexistent')
|
|
expect(store.jobs.size).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('computed getters', () => {
|
|
it('jobList is sorted newest-first', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([
|
|
makeJob({ id: 'old', created_at: '2026-03-17T00:00:00Z' }),
|
|
makeJob({ id: 'new', created_at: '2026-03-18T00:00:00Z' }),
|
|
])
|
|
|
|
expect(store.jobList[0].id).toBe('new')
|
|
expect(store.jobList[1].id).toBe('old')
|
|
})
|
|
|
|
it('activeJobs filters non-terminal', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([
|
|
makeJob({ id: 'q', status: 'queued' }),
|
|
makeJob({ id: 'd', status: 'downloading' }),
|
|
makeJob({ id: 'c', status: 'completed' }),
|
|
makeJob({ id: 'f', status: 'failed' }),
|
|
])
|
|
|
|
expect(store.activeJobs.map((j) => j.id).sort()).toEqual(['d', 'q'])
|
|
})
|
|
|
|
it('completedJobs filters completed only', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([
|
|
makeJob({ id: 'c', status: 'completed' }),
|
|
makeJob({ id: 'q', status: 'queued' }),
|
|
])
|
|
|
|
expect(store.completedJobs).toHaveLength(1)
|
|
expect(store.completedJobs[0].id).toBe('c')
|
|
})
|
|
|
|
it('failedJobs filters failed only', () => {
|
|
const store = useDownloadsStore()
|
|
store.handleInit([
|
|
makeJob({ id: 'f', status: 'failed' }),
|
|
makeJob({ id: 'q', status: 'queued' }),
|
|
])
|
|
|
|
expect(store.failedJobs).toHaveLength(1)
|
|
expect(store.failedJobs[0].id).toBe('f')
|
|
})
|
|
})
|
|
|
|
describe('isTerminal', () => {
|
|
it('returns true for terminal statuses', () => {
|
|
const store = useDownloadsStore()
|
|
expect(store.isTerminal('completed')).toBe(true)
|
|
expect(store.isTerminal('failed')).toBe(true)
|
|
expect(store.isTerminal('expired')).toBe(true)
|
|
})
|
|
|
|
it('returns false for active statuses', () => {
|
|
const store = useDownloadsStore()
|
|
expect(store.isTerminal('queued')).toBe(false)
|
|
expect(store.isTerminal('downloading')).toBe(false)
|
|
expect(store.isTerminal('extracting')).toBe(false)
|
|
})
|
|
})
|
|
})
|