Skip to content

Commit

Permalink
2916: Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
steffenkleinle committed Jan 30, 2025
1 parent ef611af commit 6f58f22
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 51 deletions.
175 changes: 124 additions & 51 deletions native/src/components/__tests__/TtsContainer.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
import { act, fireEvent, RenderAPI, screen } from '@testing-library/react-native'
import { fireEvent, RenderAPI, waitFor } from '@testing-library/react-native'
import { mocked } from 'jest-mock'
import { DateTime } from 'luxon'
import React, { useEffect } from 'react'
import React, { useContext } from 'react'
import Tts from 'react-native-tts'

import { PageModel } from 'shared/api'

import buildConfig from '../../constants/buildConfig'
import useTtsPlayer from '../../hooks/useTtsPlayer'
import useSnackbar from '../../hooks/useSnackbar'
import TestingAppContext from '../../testing/TestingAppContext'
import renderWithTheme from '../../testing/render'
import TtsContainer from '../TtsContainer'
import TtsContainer, { TtsContext } from '../TtsContainer'
import Text from '../base/Text'
import TextButton from '../base/TextButton'

jest.mock('react-i18next')
jest.mock('react-native-tts')
jest.mock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'android',
select: jest.fn(),
}))

jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock')
Reanimated.useEvent = jest.fn()
return Reanimated
})
jest.mock('../../hooks/useSnackbar')

const mockBuildConfig = (tts: boolean) => {
const previous = buildConfig()
mocked(buildConfig).mockImplementation(() => ({
...previous,
featureFlags: { ...previous.featureFlags, tts },
}))
}
const dummyPage = new PageModel({
path: '/test-path',
title: 'test',
content: '<p>This is a test</p>',
lastUpdate: DateTime.now(),
})

jest.useFakeTimers()

describe('TtsContainer', () => {
const showSnackbar = jest.fn()
mocked(useSnackbar).mockImplementation(() => showSnackbar)
const sentences = ['This is my first sentence', 'Second sentence', 'Third time is the charm']

const TestChild = () => {
const { showTtsPlayer } = useTtsPlayer(dummyPage)
useEffect(() => {
showTtsPlayer()
}, [showTtsPlayer])
return null
const { setSentences, showTtsPlayer, visible } = useContext(TtsContext)
return (
<>
<TextButton onPress={() => setSentences(sentences)} text='set sentences' />
<TextButton onPress={showTtsPlayer} text='show' />
{visible && <Text>visible</Text>}
</>
)
}

const renderTtsPlayer = (): RenderAPI =>
Expand All @@ -60,37 +55,115 @@ describe('TtsContainer', () => {
jest.clearAllTimers()
})

it('should initialize TTS engine on load', async () => {
it('should do nothing if disabled', () => {
mockBuildConfig(false)
const { getByText, queryByRole } = renderTtsPlayer()
fireEvent.press(getByText('show'))
expect(showSnackbar).not.toHaveBeenCalled()
expect(Tts.getInitStatus).not.toHaveBeenCalled()
expect(queryByRole('button', { name: 'play' })).toBeFalsy()
})

it('should show snackbar if no sentences set', () => {
mockBuildConfig(true)
renderTtsPlayer()
expect(Tts.getInitStatus).toHaveBeenCalled()
const { getByText, queryByRole } = renderTtsPlayer()
fireEvent.press(getByText('show'))
expect(showSnackbar).toHaveBeenCalledTimes(1)
expect(showSnackbar).toHaveBeenCalledWith({ text: 'nothingToReadFullMessage' })
expect(Tts.getInitStatus).not.toHaveBeenCalled()
expect(queryByRole('button', { name: 'play' })).toBeFalsy()
})

it('should start reading when the button is pressed', async () => {
renderTtsPlayer()
it('should show tts player if enabled and sentences set', async () => {
mockBuildConfig(true)
const { getByText, getByRole } = renderTtsPlayer()
fireEvent.press(getByText('set sentences'))
fireEvent.press(getByText('show'))
expect(showSnackbar).not.toHaveBeenCalled()
expect(Tts.getInitStatus).toHaveBeenCalledTimes(1)
await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())
})

// Advance any pending timers or effects
act(() => {
jest.runAllTimers()
})
it('should start playing and pause when the button is pressed', async () => {
mockBuildConfig(true)
const { getByText, getByRole } = renderTtsPlayer()
fireEvent.press(getByText('set sentences'))
fireEvent.press(getByText('show'))

const playButton = screen.getByRole('button', { name: 'play' })
fireEvent.press(playButton)
expect(Tts.stop).toHaveBeenCalledTimes(1)
await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())

expect(Tts.speak).toHaveBeenCalledWith(
'test',
expect.objectContaining({
androidParams: expect.any(Object),
iosVoiceId: '',
rate: 1,
}),
)
fireEvent.press(getByRole('button', { name: 'play' }))
await waitFor(() => expect(getByRole('button', { name: 'pause' })).toBeTruthy())
expect(Tts.speak).toHaveBeenCalledTimes(1)
expect(Tts.speak).toHaveBeenCalledWith(sentences[0], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(2)

fireEvent.press(getByRole('button', { name: 'pause' }))
await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())
expect(Tts.stop).toHaveBeenCalledTimes(3)
})

it('should remove TTS listeners on unmount', () => {
it('should close the player', async () => {
mockBuildConfig(true)
const { unmount } = renderTtsPlayer()
unmount()
expect(Tts.removeAllListeners).toHaveBeenCalledWith('tts-finish')
const { getByText, queryByText, getByRole, queryByRole } = renderTtsPlayer()
fireEvent.press(getByText('set sentences'))
fireEvent.press(getByText('show'))

expect(Tts.stop).toHaveBeenCalledTimes(1)
await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())

fireEvent.press(getByRole('button', { name: 'play' }))
await waitFor(() => expect(getByRole('button', { name: 'pause' })).toBeTruthy())
expect(Tts.stop).toHaveBeenCalledTimes(2)

expect(getByText('visible')).toBeTruthy()
fireEvent.press(getByText('common:close'))
expect(queryByRole('button', { name: 'play' })).toBeFalsy()
expect(Tts.stop).toHaveBeenCalledTimes(3)
expect(queryByText('visible')).toBeFalsy()
})

it('should play previous and next sentences', async () => {
mockBuildConfig(true)
const { getByText, getByRole } = renderTtsPlayer()
fireEvent.press(getByText('set sentences'))
fireEvent.press(getByText('show'))

await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())

fireEvent.press(getByRole('button', { name: 'play' }))
await waitFor(() => expect(getByRole('button', { name: 'pause' })).toBeTruthy())
expect(Tts.stop).toHaveBeenCalledTimes(2)

fireEvent.press(getByRole('button', { name: 'previous' }))
await waitFor(() => expect(Tts.speak).toHaveBeenCalledTimes(2))
expect(Tts.speak).toHaveBeenLastCalledWith(sentences[0], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(3)

fireEvent.press(getByRole('button', { name: 'next' }))
await waitFor(() => expect(Tts.speak).toHaveBeenCalledTimes(3))
expect(Tts.speak).toHaveBeenLastCalledWith(sentences[1], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(4)

fireEvent.press(getByRole('button', { name: 'next' }))
await waitFor(() => expect(Tts.speak).toHaveBeenCalledTimes(4))
expect(Tts.speak).toHaveBeenCalledWith(sentences[2], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(5)

fireEvent.press(getByRole('button', { name: 'previous' }))
await waitFor(() => expect(Tts.speak).toHaveBeenCalledTimes(5))
expect(Tts.speak).toHaveBeenCalledWith(sentences[1], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(6)

fireEvent.press(getByRole('button', { name: 'pause' }))
await waitFor(() => expect(getByRole('button', { name: 'play' })).toBeTruthy())
expect(Tts.stop).toHaveBeenCalledTimes(7)

fireEvent.press(getByRole('button', { name: 'play' }))
await waitFor(() => expect(getByRole('button', { name: 'pause' })).toBeTruthy())
expect(Tts.speak).toHaveBeenCalledTimes(6)
expect(Tts.speak).toHaveBeenLastCalledWith(sentences[1], expect.objectContaining({}))
expect(Tts.stop).toHaveBeenCalledTimes(8)
})
})
65 changes: 65 additions & 0 deletions native/src/hooks/__tests__/useTtsPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { fireEvent, RenderAPI, waitFor } from '@testing-library/react-native'
import { DateTime } from 'luxon'
import React from 'react'
import Tts from 'react-native-tts'

import { PageModel } from 'shared/api'

import { TtsContext } from '../../components/TtsContainer'
import TestingAppContext from '../../testing/TestingAppContext'
import renderWithTheme from '../../testing/render'
import useTtsPlayer from '../useTtsPlayer'

jest.mock('react-i18next')
jest.mock('react-native-tts')
jest.mock('../../hooks/useSnackbar')
jest.mock('../../components/TtsContainer')

jest.useFakeTimers()

describe('useTtsPlayer', () => {
const setSentences = jest.fn()
const oldSentences = ['old sentence 1.', 'old sentence 2.']
const newSentences = ['new sentence 1.', 'new sentence 2.']

const dummyPage = new PageModel({
path: '/test-path',
title: 'Test title',
content: `<div></div><div>${newSentences[0]} ${newSentences[1]}</p></div>`,
lastUpdate: DateTime.now(),
})

beforeEach(jest.clearAllMocks)

const TestChild = ({ model }: { model?: PageModel }) => {
useTtsPlayer(model)
return null
}

const render = (model?: PageModel): RenderAPI =>
renderWithTheme(
<TestingAppContext languageCode='en'>
<TtsContext.Provider value={{ setSentences, enabled: true, sentences: oldSentences, showTtsPlayer: jest.fn() }}>
<TestChild model={model} />
</TtsContext.Provider>
</TestingAppContext>,
)

it('should set new sentences and restore old sentences', () => {
const { unmount } = render(dummyPage)
expect(setSentences).toHaveBeenCalledTimes(1)
expect(setSentences).toHaveBeenCalledWith(['Test title', ...newSentences])
unmount()
expect(setSentences).toHaveBeenCalledTimes(2)
expect(setSentences).toHaveBeenLastCalledWith(oldSentences)
})

it('should set empty sentences and restore old sentences', () => {
const { unmount } = render()
expect(setSentences).toHaveBeenCalledTimes(1)
expect(setSentences).toHaveBeenCalledWith([])
unmount()
expect(setSentences).toHaveBeenCalledTimes(2)
expect(setSentences).toHaveBeenLastCalledWith(oldSentences)
})
})

0 comments on commit 6f58f22

Please sign in to comment.