3d09174c16
Closes H4 and the remaining formatter half of H2. - New @shared/format.ts is the single home for fmtBytes/fmtSpeed/fmtEta, imported by both main and renderer. main/lib/formatters.ts re-exports them. - DownloadProgress now carries raw speedBytesPerSec/etaSeconds instead of pre-formatted speed/eta strings. main's parseProgress emits the raw numbers. - The renderer stores the raw numbers on DownloadItem; QueueItem formats them for per-item display, and summarizeQueue sums/maxes them directly. The lossy string->number->string round-trip in queueStats (parseSpeed / parseEtaSeconds / a local formatSpeed / formatEta) is deleted, along with a duplicate fmtEta in store/downloads.ts. - Per-item and aggregate speed now use the same 1024-based scale; the aggregate previously used a 1000-based formatter (a latent inconsistency). Tests updated for the raw-number contract (parseProgress, summarizeQueue). typecheck + 248 tests + eslint + prettier green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { summarizeQueue, sameVideo } from '../src/renderer/src/store/queueStats'
|
|
import type { DownloadItem } from '../src/renderer/src/store/downloads'
|
|
|
|
// Minimal item factory — only the fields summarizeQueue reads.
|
|
function item(overrides: Partial<DownloadItem>): DownloadItem {
|
|
return {
|
|
id: Math.random().toString(36).slice(2),
|
|
url: 'https://youtube.com/watch?v=x',
|
|
title: 'x',
|
|
kind: 'video',
|
|
quality: '1080p',
|
|
status: 'queued',
|
|
progress: 0,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('summarizeQueue', () => {
|
|
it('counts statuses and flags active', () => {
|
|
const s = summarizeQueue([
|
|
item({ status: 'downloading', progress: 0.5 }),
|
|
item({ status: 'queued' }),
|
|
item({ status: 'error' }),
|
|
item({ status: 'completed', progress: 1 }),
|
|
item({ status: 'canceled' })
|
|
])
|
|
expect(s.downloading).toBe(1)
|
|
expect(s.queued).toBe(1)
|
|
expect(s.failed).toBe(1)
|
|
expect(s.active).toBe(true)
|
|
})
|
|
|
|
it('is inactive and zeroed when nothing is downloading or queued', () => {
|
|
const s = summarizeQueue([item({ status: 'completed', progress: 1 })])
|
|
expect(s.active).toBe(false)
|
|
expect(s.progress).toBe(0)
|
|
expect(s.speedLabel).toBe('')
|
|
expect(s.etaLabel).toBe('')
|
|
})
|
|
|
|
it('averages progress over downloading + queued (queued counts as 0%)', () => {
|
|
// one at 80%, one queued at 0% → 40%
|
|
const s = summarizeQueue([
|
|
item({ status: 'downloading', progress: 0.8 }),
|
|
item({ status: 'queued' })
|
|
])
|
|
expect(s.progress).toBeCloseTo(0.4, 5)
|
|
})
|
|
|
|
it('sums raw download rates into one formatted label', () => {
|
|
const MB = 1024 * 1024
|
|
const s = summarizeQueue([
|
|
item({ status: 'downloading', progress: 0.1, speedBytesPerSec: 4 * MB }),
|
|
item({ status: 'downloading', progress: 0.1, speedBytesPerSec: 2 * MB })
|
|
])
|
|
// 4 MB/s + 2 MB/s = 6 MB/s
|
|
expect(s.speedLabel).toBe('6.0 MB/s')
|
|
})
|
|
|
|
it('leaves the speed label empty when no item reports a rate', () => {
|
|
const s = summarizeQueue([item({ status: 'downloading', progress: 0.1 })])
|
|
expect(s.speedLabel).toBe('')
|
|
})
|
|
|
|
it('reports the longest active ETA as the time left', () => {
|
|
const s = summarizeQueue([
|
|
item({ status: 'downloading', progress: 0.5, etaSeconds: 30 }),
|
|
item({ status: 'downloading', progress: 0.2, etaSeconds: 130 })
|
|
])
|
|
expect(s.etaLabel).toBe('2:10')
|
|
})
|
|
})
|
|
|
|
describe('sameVideo', () => {
|
|
it('matches identical URLs', () => {
|
|
expect(sameVideo('https://x.com/a', 'https://x.com/a')).toBe(true)
|
|
})
|
|
|
|
it('matches YouTube links by video id across watch/youtu.be/extra params', () => {
|
|
expect(
|
|
sameVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://youtu.be/dQw4w9WgXcQ')
|
|
).toBe(true)
|
|
expect(
|
|
sameVideo(
|
|
'https://youtube.com/watch?v=dQw4w9WgXcQ&list=PL123',
|
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('does not match different videos', () => {
|
|
expect(
|
|
sameVideo(
|
|
'https://youtube.com/watch?v=aaaaaaaaaaa',
|
|
'https://youtube.com/watch?v=bbbbbbbbbbb'
|
|
)
|
|
).toBe(false)
|
|
})
|
|
|
|
it('normalises www. and trailing slash for non-YouTube URLs', () => {
|
|
expect(sameVideo('https://www.vimeo.com/123/', 'https://vimeo.com/123')).toBe(true)
|
|
})
|
|
})
|