Skip to content

Commit fa1f4f1

Browse files
committed
test: add image-preprocessor + file coverage → overall 87.14%
1 parent 0724e28 commit fa1f4f1

2 files changed

Lines changed: 242 additions & 0 deletions

File tree

src/utils/file.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { openFileDialog, saveFileDialog, writeTextFile, readTextFile, getFileInfo, exportSubtitles, type FileInfo } from '@/utils/file'
3+
4+
vi.mock('@tauri-apps/api/core', () => ({
5+
invoke: vi.fn(),
6+
}))
7+
8+
const { invoke } = await import('@tauri-apps/api/core')
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks()
12+
})
13+
14+
describe('openFileDialog', () => {
15+
it('returns path on success', async () => {
16+
;(invoke as any).mockResolvedValue('/path/to/file.mp4')
17+
const result = await openFileDialog('Open Video')
18+
expect(result).toBe('/path/to/file.mp4')
19+
expect(invoke).toHaveBeenCalledWith('open_file_dialog', { title: 'Open Video', filters: [] })
20+
})
21+
22+
it('returns null on error', async () => {
23+
;(invoke as any).mockRejectedValue(new Error('dialog failed'))
24+
const result = await openFileDialog('Open Video')
25+
expect(result).toBeNull()
26+
})
27+
})
28+
29+
describe('saveFileDialog', () => {
30+
it('returns path on success', async () => {
31+
;(invoke as any).mockResolvedValue('/path/to/output.srt')
32+
const result = await saveFileDialog('Save', 'output.srt')
33+
expect(result).toBe('/path/to/output.srt')
34+
expect(invoke).toHaveBeenCalledWith('save_file_dialog', { title: 'Save', defaultName: 'output.srt', filters: [] })
35+
})
36+
37+
it('returns null on error', async () => {
38+
;(invoke as any).mockRejectedValue(new Error('save failed'))
39+
const result = await saveFileDialog('Save')
40+
expect(result).toBeNull()
41+
})
42+
})
43+
44+
describe('writeTextFile', () => {
45+
it('returns true on success', async () => {
46+
;(invoke as any).mockResolvedValue(undefined)
47+
const result = await writeTextFile('/path/file.srt', 'subtitle content')
48+
expect(result).toBe(true)
49+
expect(invoke).toHaveBeenCalledWith('write_text_file', { path: '/path/file.srt', content: 'subtitle content' })
50+
})
51+
52+
it('returns false on error', async () => {
53+
;(invoke as any).mockRejectedValue(new Error('write failed'))
54+
const result = await writeTextFile('/path/file.srt', 'content')
55+
expect(result).toBe(false)
56+
})
57+
})
58+
59+
describe('readTextFile', () => {
60+
it('returns content on success', async () => {
61+
;(invoke as any).mockResolvedValue('file content')
62+
const result = await readTextFile('/path/file.srt')
63+
expect(result).toBe('file content')
64+
expect(invoke).toHaveBeenCalledWith('read_text_file', { path: '/path/file.srt' })
65+
})
66+
67+
it('returns null on error', async () => {
68+
;(invoke as any).mockRejectedValue(new Error('read failed'))
69+
const result = await readTextFile('/path/file.srt')
70+
expect(result).toBeNull()
71+
})
72+
})
73+
74+
describe('getFileInfo', () => {
75+
it('returns FileInfo on success', async () => {
76+
const info: FileInfo = { path: '/path/file.mp4', name: 'file.mp4', size: 1024, is_file: true, is_dir: false }
77+
;(invoke as any).mockResolvedValue(info)
78+
const result = await getFileInfo('/path/file.mp4')
79+
expect(result).toEqual(info)
80+
expect(invoke).toHaveBeenCalledWith('get_file_info', { path: '/path/file.mp4' })
81+
})
82+
83+
it('returns null on error', async () => {
84+
;(invoke as any).mockRejectedValue(new Error('info failed'))
85+
const result = await getFileInfo('/path/file.mp4')
86+
expect(result).toBeNull()
87+
})
88+
})
89+
90+
describe('exportSubtitles', () => {
91+
it('returns filePath when save succeeds', async () => {
92+
;(invoke as any).mockResolvedValue('/path/output.srt')
93+
const result = await exportSubtitles('subtitle content', 'srt', 'output')
94+
expect(result).toBe('/path/output.srt')
95+
})
96+
97+
it('returns null when save dialog is cancelled', async () => {
98+
;(invoke as any).mockResolvedValue(null)
99+
const result = await exportSubtitles('content', 'srt')
100+
expect(result).toBeNull()
101+
})
102+
103+
it('returns null when write fails', async () => {
104+
;(invoke as any).mockResolvedValue('/path/output.srt')
105+
;(invoke as any).mockRejectedValueOnce(new Error('write failed'))
106+
const result = await exportSubtitles('content', 'srt')
107+
expect(result).toBeNull()
108+
})
109+
})
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { preprocessImage, preprocessForSubtitles, preprocessForGeneralText, DEFAULT_PREPROCESSOR_CONFIG } from '@/utils/image-preprocessor'
3+
import { makeFrame } from '@/test-utils/frames'
4+
5+
// Mock canvas and context
6+
const mockCanvas = {
7+
width: 0,
8+
height: 0,
9+
getContext: vi.fn(() => ({
10+
putImageData: vi.fn(),
11+
})),
12+
toDataURL: vi.fn(() => 'data:image/png;base64,mock'),
13+
toBlob: vi.fn((cb: (blob: Blob) => void) => cb(new Blob(['mock'], { type: 'image/png' }))),
14+
}
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks()
18+
vi.stubGlobal('document', {
19+
createElement: vi.fn((tag: string) => {
20+
if (tag === 'canvas') return mockCanvas
21+
return {}
22+
}),
23+
})
24+
vi.stubGlobal('HTMLCanvasElement', vi.fn(() => mockCanvas))
25+
})
26+
27+
describe('preprocessImage', () => {
28+
const input = makeFrame(40, 40, 128, 128, 128)
29+
30+
it('returns PreprocessorResult with processedData, canvas, toDataURL, toBlob', () => {
31+
const result = preprocessImage(input)
32+
expect(result.processedData).toBeInstanceOf(ImageData)
33+
expect(result.processedData.width).toBeGreaterThan(0)
34+
expect(result.processedData.height).toBeGreaterThan(0)
35+
expect(typeof result.toDataURL).toBe('function')
36+
expect(typeof result.toBlob).toBe('function')
37+
})
38+
39+
it('produces output larger than input with default config (scale + deskew)', () => {
40+
const result = preprocessImage(input)
41+
expect(result.processedData.width).toBeGreaterThan(input.width)
42+
expect(result.processedData.height).toBeGreaterThan(input.height)
43+
})
44+
45+
it('skips deskew when deskew=false', () => {
46+
const result = preprocessImage(input, { deskew: false })
47+
expect(result.processedData.width).toBeGreaterThan(0)
48+
expect(result.processedData.height).toBeGreaterThan(0)
49+
})
50+
51+
it('skips upscale when scaleFactor=1', () => {
52+
const result = preprocessImage(input, { scaleFactor: 1, deskew: false })
53+
// With scale=1 and no deskew, output should be same size as input
54+
expect(result.processedData.width).toBe(input.width)
55+
expect(result.processedData.height).toBe(input.height)
56+
})
57+
58+
it('skips denoise when denoise=false', () => {
59+
const result = preprocessImage(input, { denoise: false })
60+
expect(result.processedData.width).toBeGreaterThan(0)
61+
expect(result.processedData.height).toBeGreaterThan(0)
62+
})
63+
64+
it('skips threshold when adaptiveThreshold=false', () => {
65+
const result = preprocessImage(input, { adaptiveThreshold: false })
66+
expect(result.processedData.width).toBeGreaterThan(0)
67+
expect(result.processedData.height).toBeGreaterThan(0)
68+
})
69+
70+
it('skips morph cleanup when morphCleanup=false', () => {
71+
const result = preprocessImage(input, { morphCleanup: false })
72+
expect(result.processedData.width).toBeGreaterThan(0)
73+
expect(result.processedData.height).toBeGreaterThan(0)
74+
})
75+
76+
it('applies invertColors when enabled', () => {
77+
const result = preprocessImage(input, { invertColors: true, scaleFactor: 1, deskew: false })
78+
const normal = preprocessImage(input, { invertColors: false, scaleFactor: 1, deskew: false })
79+
expect(result.processedData.data[0]).not.toBe(normal.processedData.data[0])
80+
})
81+
82+
it('preserves alpha channel through pipeline', () => {
83+
const semiTransparent = makeFrame(10, 10, 200, 200, 200)
84+
semiTransparent.data[3] = 128
85+
const result = preprocessImage(semiTransparent, { scaleFactor: 1, deskew: false })
86+
expect(result.processedData.data[3]).toBeGreaterThanOrEqual(0)
87+
expect(result.processedData.data[3]).toBeLessThanOrEqual(255)
88+
})
89+
90+
it('toDataURL returns a data URL string', async () => {
91+
const result = preprocessImage(input)
92+
const url = await result.toDataURL()
93+
expect(url).toContain('data:image/png')
94+
})
95+
96+
it('toBlob resolves with a Blob', async () => {
97+
const result = preprocessImage(input)
98+
const blob = await result.toBlob()
99+
expect(blob).toBeInstanceOf(Blob)
100+
})
101+
})
102+
103+
describe('preprocessForSubtitles', () => {
104+
it('uses subtitle-optimized defaults', () => {
105+
const result = preprocessForSubtitles(makeFrame(40, 40, 128, 128, 128))
106+
expect(result.processedData.width).toBeGreaterThan(0)
107+
expect(result.processedData.height).toBeGreaterThan(0)
108+
})
109+
})
110+
111+
describe('preprocessForGeneralText', () => {
112+
it('uses general-text defaults with multiPass=false', () => {
113+
const result = preprocessForGeneralText(makeFrame(40, 40, 128, 128, 128))
114+
expect(result.processedData.width).toBeGreaterThan(0)
115+
expect(result.processedData.height).toBeGreaterThan(0)
116+
})
117+
})
118+
119+
describe('DEFAULT_PREPROCESSOR_CONFIG', () => {
120+
it('has expected default values', () => {
121+
expect(DEFAULT_PREPROCESSOR_CONFIG.scaleFactor).toBe(2.0)
122+
expect(DEFAULT_PREPROCESSOR_CONFIG.enhanceContrast).toBe(true)
123+
expect(DEFAULT_PREPROCESSOR_CONFIG.contrastLevel).toBe(1.5)
124+
expect(DEFAULT_PREPROCESSOR_CONFIG.adaptiveThreshold).toBe(true)
125+
expect(DEFAULT_PREPROCESSOR_CONFIG.adaptiveBlockSize).toBe(11)
126+
expect(DEFAULT_PREPROCESSOR_CONFIG.denoise).toBe(true)
127+
expect(DEFAULT_PREPROCESSOR_CONFIG.morphCleanup).toBe(true)
128+
expect(DEFAULT_PREPROCESSOR_CONFIG.invertColors).toBe(false)
129+
expect(DEFAULT_PREPROCESSOR_CONFIG.deskew).toBe(true)
130+
expect(DEFAULT_PREPROCESSOR_CONFIG.multiPass).toBe(true)
131+
expect(DEFAULT_PREPROCESSOR_CONFIG.multiPassScale).toBe(3.0)
132+
})
133+
})

0 commit comments

Comments
 (0)