flnxi-Wu/lib/__tests__/audio-parser-browser.test.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

424 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 浏览器端音频解析模块测试
* 测试 lib/audio-parser-browser.ts 的核心功能
*/
// Mock music-metadata-browser
jest.mock('music-metadata-browser', () => ({
parseBlob: jest.fn(),
}))
import {
formatDuration,
isAudioFile,
parseAudioFile,
AUDIO_ACCEPT_TYPES,
AUDIO_FILE_EXTENSIONS,
AudioMetadataBrowser,
} from '../audio-parser-browser'
const mockParseBlob = jest.requireMock('music-metadata-browser').parseBlob as jest.Mock
describe('浏览器端音频解析模块', () => {
describe('formatDuration', () => {
it('应该正确格式化0秒', () => {
expect(formatDuration(0)).toBe('00:00')
})
it('应该正确格式化小于1分钟的时长', () => {
expect(formatDuration(30)).toBe('00:30')
expect(formatDuration(59)).toBe('00:59')
})
it('应该正确格式化整分钟', () => {
expect(formatDuration(60)).toBe('01:00')
expect(formatDuration(120)).toBe('02:00')
expect(formatDuration(600)).toBe('10:00')
})
it('应该正确格式化分钟和秒的组合', () => {
expect(formatDuration(90)).toBe('01:30')
expect(formatDuration(125)).toBe('02:05')
expect(formatDuration(3661)).toBe('61:01') // 超过1小时
})
it('应该正确处理小数秒(向下取整)', () => {
expect(formatDuration(30.5)).toBe('00:30')
expect(formatDuration(59.9)).toBe('00:59')
expect(formatDuration(90.7)).toBe('01:30')
})
})
describe('isAudioFile', () => {
it('应该通过MIME类型识别音频文件', () => {
const audioFile = new File([''], 'test.mp3', { type: 'audio/mpeg' })
expect(isAudioFile(audioFile)).toBe(true)
})
it('应该通过文件扩展名识别音频文件', () => {
const mp3File = new File([''], 'test.mp3', { type: '' })
const wavFile = new File([''], 'test.wav', { type: '' })
const flacFile = new File([''], 'test.flac', { type: '' })
const aacFile = new File([''], 'test.aac', { type: '' })
const m4aFile = new File([''], 'test.m4a', { type: '' })
const oggFile = new File([''], 'test.ogg', { type: '' })
expect(isAudioFile(mp3File)).toBe(true)
expect(isAudioFile(wavFile)).toBe(true)
expect(isAudioFile(flacFile)).toBe(true)
expect(isAudioFile(aacFile)).toBe(true)
expect(isAudioFile(m4aFile)).toBe(true)
expect(isAudioFile(oggFile)).toBe(true)
})
it('应该拒绝非音频文件', () => {
const imageFile = new File([''], 'test.png', { type: 'image/png' })
const videoFile = new File([''], 'test.mp4', { type: 'video/mp4' })
const textFile = new File([''], 'test.txt', { type: 'text/plain' })
expect(isAudioFile(imageFile)).toBe(false)
expect(isAudioFile(videoFile)).toBe(false)
expect(isAudioFile(textFile)).toBe(false)
})
it('应该处理没有扩展名的文件', () => {
const noExtFile = new File([''], 'test', { type: '' })
expect(isAudioFile(noExtFile)).toBe(false)
})
})
describe('AUDIO_ACCEPT_TYPES', () => {
it('应该包含常见音频MIME类型', () => {
expect(AUDIO_ACCEPT_TYPES).toContain('audio/mpeg')
expect(AUDIO_ACCEPT_TYPES).toContain('audio/wav')
expect(AUDIO_ACCEPT_TYPES).toContain('audio/flac')
expect(AUDIO_ACCEPT_TYPES).toContain('audio/aac')
expect(AUDIO_ACCEPT_TYPES).toContain('audio/ogg')
})
it('应该包含常见音频文件扩展名', () => {
expect(AUDIO_ACCEPT_TYPES).toContain('.mp3')
expect(AUDIO_ACCEPT_TYPES).toContain('.wav')
expect(AUDIO_ACCEPT_TYPES).toContain('.flac')
expect(AUDIO_ACCEPT_TYPES).toContain('.aac')
expect(AUDIO_ACCEPT_TYPES).toContain('.m4a')
expect(AUDIO_ACCEPT_TYPES).toContain('.ogg')
})
})
describe('AUDIO_FILE_EXTENSIONS', () => {
it('应该包含所有支持的音频扩展名', () => {
const expectedExtensions = ['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg', 'wma', 'aiff', 'ape', 'webm']
expectedExtensions.forEach(ext => {
expect(AUDIO_FILE_EXTENSIONS).toContain(ext)
})
})
it('应该有正确的扩展名数量', () => {
expect(AUDIO_FILE_EXTENSIONS.length).toBe(10)
})
})
describe('parseAudioFile', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('应该正确解析音频文件元数据', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 180.5,
container: 'MPEG',
bitrate: 320000,
sampleRate: 44100,
numberOfChannels: 2,
},
common: {
title: 'Test Song',
artist: 'Test Artist',
album: 'Test Album',
year: 2024,
genre: ['Pop', 'Rock'],
},
})
const file = new File(['test audio data'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.duration).toBe(180.5)
expect(result?.durationFormatted).toBe('03:00')
expect(result?.format).toBe('MPEG')
expect(result?.bitrate).toBe(320000)
expect(result?.sampleRate).toBe(44100)
expect(result?.channels).toBe(2)
expect(result?.title).toBe('Test Song')
expect(result?.artist).toBe('Test Artist')
expect(result?.album).toBe('Test Album')
expect(result?.year).toBe(2024)
expect(result?.genre).toEqual(['Pop', 'Rock'])
})
it('应该处理缺少duration的情况', async () => {
mockParseBlob.mockResolvedValue({
format: {
container: 'WAV',
},
common: {},
})
const file = new File(['test'], 'test.wav', { type: 'audio/wav' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.duration).toBe(0)
expect(result?.durationFormatted).toBe('00:00')
})
it('应该处理缺少container的情况', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 60,
},
common: {},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.format).toBe('unknown')
})
it('应该优雅处理解析错误', async () => {
mockParseBlob.mockRejectedValue(new Error('Invalid audio format'))
const file = new File(['invalid data'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).toBeNull()
})
it('应该提取内嵌封面图', async () => {
const mockPictureData = new Uint8Array([0x89, 0x50, 0x4E, 0x47]) // PNG header
mockParseBlob.mockResolvedValue({
format: {
duration: 120,
container: 'MP3',
},
common: {
picture: [{
format: 'image/png',
data: mockPictureData,
}],
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.coverImage).toBeDefined()
expect(result?.coverImage).toMatch(/^data:image\/png;base64,/)
})
it('应该处理没有封面图的情况', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 60,
container: 'MP3',
},
common: {},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.coverImage).toBeUndefined()
})
})
describe('AudioMetadataBrowser 类型验证', () => {
it('应该有正确的类型结构', () => {
const mockMetadata: AudioMetadataBrowser = {
duration: 180,
durationFormatted: '03:00',
format: 'mp3',
bitrate: 320000,
sampleRate: 44100,
channels: 2,
title: 'Test',
artist: 'Artist',
album: 'Album',
year: 2024,
genre: ['Pop'],
coverImage: 'data:image/png;base64,xxx',
}
expect(mockMetadata.duration).toBe(180)
expect(mockMetadata.durationFormatted).toBe('03:00')
expect(mockMetadata.format).toBe('mp3')
expect(mockMetadata.title).toBe('Test')
})
it('可选字段应该可以为undefined', () => {
const minimalMetadata: AudioMetadataBrowser = {
duration: 60,
durationFormatted: '01:00',
format: 'wav',
}
expect(minimalMetadata.bitrate).toBeUndefined()
expect(minimalMetadata.sampleRate).toBeUndefined()
expect(minimalMetadata.channels).toBeUndefined()
expect(minimalMetadata.title).toBeUndefined()
expect(minimalMetadata.artist).toBeUndefined()
expect(minimalMetadata.coverImage).toBeUndefined()
})
})
describe('GBK编码乱码修复', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('应该正确处理正常的英文标签(不触发GBK解码)', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
title: 'Normal English Title',
artist: 'Normal Artist',
album: 'Normal Album',
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.title).toBe('Normal English Title')
expect(result?.artist).toBe('Normal Artist')
expect(result?.album).toBe('Normal Album')
})
it('应该正确处理正常的中文标签(UTF-8编码)', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
title: '中文歌曲名称',
artist: '中文艺术家',
album: '中文专辑',
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.title).toBe('中文歌曲名称')
expect(result?.artist).toBe('中文艺术家')
expect(result?.album).toBe('中文专辑')
})
it('应该尝试修复GBK编码的乱码标签', async () => {
// 模拟 GBK 编码的"测试"被错误解码为 ISO-8859-1 后的乱码
// "测试" 的 GBK 编码是 0xB2 0xE2 0xCA 0xD4
// 被当作 ISO-8859-1 解码后变成 "²âÊÔ"
const garbledTitle = String.fromCharCode(0xB2, 0xE2, 0xCA, 0xD4)
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
title: garbledTitle,
artist: 'Normal Artist',
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
// 应该被解码为中文"测试"
expect(result?.title).toBe('测试')
// 正常的英文不应该被改变
expect(result?.artist).toBe('Normal Artist')
})
it('应该处理空标签', async () => {
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
title: undefined,
artist: '',
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.title).toBeUndefined()
expect(result?.artist).toBe('')
})
it('应该处理流派数组中的乱码', async () => {
// 模拟 GBK 编码的"流行"被错误解码
// "流行" 的 GBK 编码是 0xC1 0xF7 0xD0 0xD0
const garbledGenre = String.fromCharCode(0xC1, 0xF7, 0xD0, 0xD0)
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
genre: [garbledGenre, 'Rock'],
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
expect(result?.genre).toContain('流行')
expect(result?.genre).toContain('Rock')
})
it('应该处理低比例高位字符的字符串(不触发GBK解码)', async () => {
// 模拟一个高位字符比例低于30%的字符串(不会触发GBK解码)
// 例如:10个字符中只有2个高位字符(20%)
const mixedString = 'Hello' + String.fromCharCode(0xB2, 0xE2) + 'abc'
mockParseBlob.mockResolvedValue({
format: {
duration: 180,
container: 'MP3',
},
common: {
title: mixedString,
},
})
const file = new File(['test'], 'test.mp3', { type: 'audio/mpeg' })
const result = await parseAudioFile(file)
expect(result).not.toBeNull()
// 高位字符比例低于30%,不会触发GBK解码,保持原样
expect(result?.title).toBe(mixedString)
})
})
})