mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
test: improve coverage for some test files (#32916)
Signed-off-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Poojan <poojan@infocusp.com> Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Pandaaaa906 <ye.pandaaaa906@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ijas <ijas.ahmd.ap@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: 木之本澪 <kinomotomiovo@gmail.com> Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com> Co-authored-by: 不做了睡大觉 <64798754+stakeswky@users.noreply.github.com> Co-authored-by: User <user@example.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Leilei <138381132+Inlei@users.noreply.github.com> Co-authored-by: HaKu <104669497+haku-ink@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Varun Chawla <34209028+veeceey@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: tda <95275462+tda1017@users.noreply.github.com> Co-authored-by: root <root@DESKTOP-KQLO90N> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: slegarraga <64795732+slegarraga@users.noreply.github.com> Co-authored-by: 99 <wh2099@pm.me> Co-authored-by: Br1an <932039080@qq.com> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: akkoaya <151345394+akkoaya@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: weiguang li <codingpunk@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: HanWenbo <124024253+hwb96@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Stable Genius <stablegenius043@gmail.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Co-authored-by: ふるい <46769295+Echo0ff@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
@ -1,10 +1,9 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { audioToText } from '@/service/share'
|
||||
import VoiceInput from '../index'
|
||||
|
||||
const { mockState, MockRecorder } = vi.hoisted(() => {
|
||||
const { mockState, MockRecorder, rafState } = vi.hoisted(() => {
|
||||
const state = {
|
||||
params: {} as Record<string, string>,
|
||||
pathname: '/test',
|
||||
@ -12,6 +11,9 @@ const { mockState, MockRecorder } = vi.hoisted(() => {
|
||||
startOverride: null as (() => Promise<void>) | null,
|
||||
analyseData: new Uint8Array(1024).fill(150) as Uint8Array,
|
||||
}
|
||||
const rafStateObj = {
|
||||
callback: null as (() => void) | null,
|
||||
}
|
||||
|
||||
class MockRecorderClass {
|
||||
start = vi.fn((..._args: unknown[]) => {
|
||||
@ -33,7 +35,7 @@ const { mockState, MockRecorder } = vi.hoisted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
return { mockState: state, MockRecorder: MockRecorderClass }
|
||||
return { mockState: state, MockRecorder: MockRecorderClass, rafState: rafStateObj }
|
||||
})
|
||||
|
||||
vi.mock('js-audio-recorder', () => ({
|
||||
@ -54,6 +56,17 @@ vi.mock('../utils', () => ({
|
||||
convertToMp3: vi.fn(() => new Blob(['test'], { type: 'audio/mp3' })),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('ahooks')>()
|
||||
return {
|
||||
...actual,
|
||||
useRafInterval: vi.fn((fn) => {
|
||||
rafState.callback = fn
|
||||
return vi.fn()
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('VoiceInput', () => {
|
||||
const onConverted = vi.fn()
|
||||
const onCancel = vi.fn()
|
||||
@ -64,6 +77,7 @@ describe('VoiceInput', () => {
|
||||
mockState.pathname = '/test'
|
||||
mockState.recorderInstances = []
|
||||
mockState.startOverride = null
|
||||
rafState.callback = null
|
||||
|
||||
// Ensure canvas has non-zero dimensions for initCanvas()
|
||||
HTMLCanvasElement.prototype.getBoundingClientRect = vi.fn(() => ({
|
||||
@ -257,4 +271,268 @@ describe('VoiceInput', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should use fallback rect when canvas roundRect is not available', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'test' })
|
||||
mockState.params = { token: 'abc' }
|
||||
mockState.analyseData = new Uint8Array(1024).fill(150)
|
||||
|
||||
const oldGetContext = HTMLCanvasElement.prototype.getContext
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
|
||||
scale: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
rect: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
})) as unknown as typeof HTMLCanvasElement.prototype.getContext
|
||||
|
||||
let rafCalls = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
rafCalls++
|
||||
if (rafCalls <= 1)
|
||||
cb(0)
|
||||
return rafCalls
|
||||
})
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await user.click(await screen.findByTestId('voice-input-stop'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onConverted).toHaveBeenCalled()
|
||||
})
|
||||
HTMLCanvasElement.prototype.getContext = oldGetContext
|
||||
})
|
||||
|
||||
it('should display timer in MM:SS format correctly', async () => {
|
||||
mockState.params = { token: 'abc' }
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const timer = await screen.findByTestId('voice-input-timer')
|
||||
expect(timer).toHaveTextContent('00:00')
|
||||
|
||||
await act(async () => {
|
||||
if (rafState.callback)
|
||||
rafState.callback()
|
||||
})
|
||||
expect(timer).toHaveTextContent('00:01')
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await act(async () => {
|
||||
if (rafState.callback)
|
||||
rafState.callback()
|
||||
})
|
||||
}
|
||||
expect(timer).toHaveTextContent('00:10')
|
||||
})
|
||||
|
||||
it('should show timer element with formatted time', async () => {
|
||||
mockState.params = { token: 'abc' }
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const timer = screen.getByTestId('voice-input-timer')
|
||||
expect(timer).toBeInTheDocument()
|
||||
// Initial state should show 00:00
|
||||
expect(timer.textContent).toMatch(/0\d:\d{2}/)
|
||||
})
|
||||
|
||||
it('should handle data values in normal range (between 128 and 178)', async () => {
|
||||
mockState.analyseData = new Uint8Array(1024).fill(150)
|
||||
|
||||
let rafCalls = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
rafCalls++
|
||||
if (rafCalls <= 2)
|
||||
cb(0)
|
||||
return rafCalls
|
||||
})
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const recorder = mockState.recorderInstances[0] as any
|
||||
expect(recorder.getRecordAnalyseData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle canvas context and device pixel ratio', async () => {
|
||||
const dprSpy = vi.spyOn(window, 'devicePixelRatio', 'get')
|
||||
dprSpy.mockReturnValue(2)
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
expect(screen.getByTestId('voice-input-stop')).toBeInTheDocument()
|
||||
|
||||
dprSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should handle empty params with no token or appId', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'test' })
|
||||
mockState.params = {}
|
||||
mockState.pathname = '/test'
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const stopBtn = await screen.findByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
// Should call audioToText with empty URL when neither token nor appId is present
|
||||
expect(audioToText).toHaveBeenCalledWith('', 'installedApp', expect.any(FormData))
|
||||
})
|
||||
})
|
||||
|
||||
it('should render speaking state indicator', async () => {
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
expect(await screen.findByText('common.voiceInput.speaking')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should cleanup on unmount', () => {
|
||||
const { unmount } = render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const recorder = mockState.recorderInstances[0] as any
|
||||
|
||||
unmount()
|
||||
|
||||
expect(recorder.stop).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle all data in recordAnalyseData for canvas drawing', async () => {
|
||||
const allDataValues = []
|
||||
for (let i = 0; i < 256; i++) {
|
||||
allDataValues.push(i)
|
||||
}
|
||||
mockState.analyseData = new Uint8Array(allDataValues)
|
||||
|
||||
let rafCalls = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
rafCalls++
|
||||
if (rafCalls <= 2)
|
||||
cb(0)
|
||||
return rafCalls
|
||||
})
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const recorder = mockState.recorderInstances[0] as any
|
||||
expect(recorder.getRecordAnalyseData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass multiple props correctly', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'test' })
|
||||
mockState.params = { token: 'token123' }
|
||||
|
||||
render(
|
||||
<VoiceInput
|
||||
onConverted={onConverted}
|
||||
onCancel={onCancel}
|
||||
wordTimestamps="enabled"
|
||||
/>,
|
||||
)
|
||||
|
||||
const stopBtn = await screen.findByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
const calls = vi.mocked(audioToText).mock.calls
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
const [url, sourceType, formData] = calls[0]
|
||||
expect(url).toBe('/audio-to-text')
|
||||
expect(sourceType).toBe('webApp')
|
||||
expect(formData.get('word_timestamps')).toBe('enabled')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle pathname with explore/installed correctly when appId exists', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'test' })
|
||||
mockState.params = { appId: 'app-id-123' }
|
||||
mockState.pathname = '/explore/installed/app-details'
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const stopBtn = await screen.findByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(audioToText).toHaveBeenCalledWith(
|
||||
'/installed-apps/app-id-123/audio-to-text',
|
||||
'installedApp',
|
||||
expect.any(FormData),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should render timer with initial 00:00 value', () => {
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const timer = screen.getByTestId('voice-input-timer')
|
||||
expect(timer).toHaveTextContent('00:00')
|
||||
})
|
||||
|
||||
it('should render stop button during recording', async () => {
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
expect(await screen.findByTestId('voice-input-stop')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render converting UI after stopping', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(audioToText).mockImplementation(() => new Promise(() => { }))
|
||||
mockState.params = { token: 'abc' }
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
const stopBtn = await screen.findByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
|
||||
await screen.findByTestId('voice-input-loader')
|
||||
expect(screen.getByTestId('voice-input-converting-text')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('voice-input-cancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should auto-stop recording and convert audio when duration reaches 10 minutes (600s)', async () => {
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'auto-stopped text' })
|
||||
mockState.params = { token: 'abc' }
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
expect(await screen.findByTestId('voice-input-stop')).toBeInTheDocument()
|
||||
|
||||
for (let i = 0; i < 601; i++) {
|
||||
await act(async () => {
|
||||
if (rafState.callback)
|
||||
rafState.callback()
|
||||
})
|
||||
}
|
||||
|
||||
expect(await screen.findByTestId('voice-input-converting-text')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(onConverted).toHaveBeenCalledWith('auto-stopped text')
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
it('should handle null canvas element gracefully during initialization', async () => {
|
||||
const getElementByIdMock = vi.spyOn(document, 'getElementById').mockReturnValue(null)
|
||||
|
||||
const { unmount } = render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
unmount()
|
||||
|
||||
getElementByIdMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should handle getContext returning null gracefully during initialization', async () => {
|
||||
const oldGetContext = HTMLCanvasElement.prototype.getContext
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(null)
|
||||
|
||||
const { unmount } = render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
unmount()
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = oldGetContext
|
||||
})
|
||||
})
|
||||
|
||||
196
web/app/components/base/voice-input/__tests__/utils.spec.ts
Normal file
196
web/app/components/base/voice-input/__tests__/utils.spec.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { convertToMp3 } from '../utils'
|
||||
|
||||
// ── Hoisted mocks ──
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const readHeader = vi.fn()
|
||||
const encodeBuffer = vi.fn()
|
||||
const flush = vi.fn()
|
||||
|
||||
return { readHeader, encodeBuffer, flush }
|
||||
})
|
||||
|
||||
vi.mock('lamejs', () => ({
|
||||
default: {
|
||||
WavHeader: {
|
||||
readHeader: mocks.readHeader,
|
||||
},
|
||||
Mp3Encoder: class MockMp3Encoder {
|
||||
encodeBuffer = mocks.encodeBuffer
|
||||
flush = mocks.flush
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('lamejs/src/js/BitStream', () => ({ default: {} }))
|
||||
vi.mock('lamejs/src/js/Lame', () => ({ default: {} }))
|
||||
vi.mock('lamejs/src/js/MPEGMode', () => ({ default: {} }))
|
||||
|
||||
// ── helpers ──
|
||||
|
||||
/** Build a fake recorder whose getChannelData returns DataView-like objects with .buffer and .byteLength. */
|
||||
function createMockRecorder(opts: {
|
||||
channels: number
|
||||
sampleRate: number
|
||||
leftSamples: number[]
|
||||
rightSamples?: number[]
|
||||
}) {
|
||||
const toDataView = (samples: number[]) => {
|
||||
const buf = new ArrayBuffer(samples.length * 2)
|
||||
const view = new DataView(buf)
|
||||
samples.forEach((v, i) => {
|
||||
view.setInt16(i * 2, v, true)
|
||||
})
|
||||
return view
|
||||
}
|
||||
|
||||
const leftView = toDataView(opts.leftSamples)
|
||||
const rightView = opts.rightSamples ? toDataView(opts.rightSamples) : null
|
||||
|
||||
mocks.readHeader.mockReturnValue({
|
||||
channels: opts.channels,
|
||||
sampleRate: opts.sampleRate,
|
||||
})
|
||||
|
||||
return {
|
||||
getWAV: vi.fn(() => new ArrayBuffer(44)),
|
||||
getChannelData: vi.fn(() => ({
|
||||
left: leftView,
|
||||
right: rightView,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
describe('convertToMp3', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should convert mono WAV data to an MP3 blob', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
leftSamples: [100, 200, 300, 400],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1, 2, 3]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([4, 5]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
expect(mocks.encodeBuffer).toHaveBeenCalled()
|
||||
// Mono: encodeBuffer called with only left data
|
||||
const firstCall = mocks.encodeBuffer.mock.calls[0]
|
||||
expect(firstCall).toHaveLength(1)
|
||||
expect(mocks.flush).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should convert stereo WAV data to an MP3 blob', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 2,
|
||||
sampleRate: 48000,
|
||||
leftSamples: [100, 200],
|
||||
rightSamples: [300, 400],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([10, 20]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([30]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
// Stereo: encodeBuffer called with left AND right
|
||||
const firstCall = mocks.encodeBuffer.mock.calls[0]
|
||||
expect(firstCall).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should skip empty encoded buffers', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
leftSamples: [100, 200],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array(0))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should include flush data when flush returns non-empty buffer', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 22050,
|
||||
leftSamples: [1],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array(0))
|
||||
mocks.flush.mockReturnValue(new Int8Array([99, 98, 97]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.size).toBe(3)
|
||||
})
|
||||
|
||||
it('should omit flush data when flush returns empty buffer', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
leftSamples: [10, 20],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1, 2]))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.size).toBe(2)
|
||||
})
|
||||
|
||||
it('should process multiple chunks when sample count exceeds maxSamples (1152)', () => {
|
||||
const samples = Array.from({ length: 2400 }, (_, i) => i % 32767)
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
leftSamples: samples,
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1]))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(mocks.encodeBuffer.mock.calls.length).toBeGreaterThan(1)
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should encode stereo with right channel subarray', () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 2,
|
||||
sampleRate: 44100,
|
||||
leftSamples: [100, 200, 300],
|
||||
rightSamples: [400, 500, 600],
|
||||
})
|
||||
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([5, 6, 7]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([8]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
for (const call of mocks.encodeBuffer.mock.calls) {
|
||||
expect(call).toHaveLength(2)
|
||||
expect(call[0]).toBeInstanceOf(Int16Array)
|
||||
expect(call[1]).toBeInstanceOf(Int16Array)
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -3,10 +3,11 @@ import BitStream from 'lamejs/src/js/BitStream'
|
||||
import Lame from 'lamejs/src/js/Lame'
|
||||
import MPEGMode from 'lamejs/src/js/MPEGMode'
|
||||
|
||||
/* v8 ignore next - @preserve */
|
||||
if (globalThis) {
|
||||
(globalThis as any).MPEGMode = MPEGMode
|
||||
;(globalThis as any).Lame = Lame
|
||||
;(globalThis as any).BitStream = BitStream
|
||||
; (globalThis as any).Lame = Lame
|
||||
; (globalThis as any).BitStream = BitStream
|
||||
}
|
||||
|
||||
export const convertToMp3 = (recorder: any) => {
|
||||
|
||||
Reference in New Issue
Block a user