diff --git a/native/src/components/__tests__/TtsContainer.spec.tsx b/native/src/components/__tests__/TtsContainer.spec.tsx
index 82ae77967a..9d3a66a23d 100644
--- a/native/src/components/__tests__/TtsContainer.spec.tsx
+++ b/native/src/components/__tests__/TtsContainer.spec.tsx
@@ -1,29 +1,20 @@
-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(() => ({
@@ -31,19 +22,23 @@ const mockBuildConfig = (tts: boolean) => {
featureFlags: { ...previous.featureFlags, tts },
}))
}
-const dummyPage = new PageModel({
- path: '/test-path',
- title: 'test',
- content: '
This is a test
',
- 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 (
+ <>
+ setSentences(sentences)} text='set sentences' />
+
+ {visible && visible}
+ >
+ )
}
const renderTtsPlayer = (): RenderAPI =>
@@ -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)
})
})
diff --git a/native/src/hooks/__tests__/useTtsPlayer.tsx b/native/src/hooks/__tests__/useTtsPlayer.tsx
new file mode 100644
index 0000000000..495c8b1a5c
--- /dev/null
+++ b/native/src/hooks/__tests__/useTtsPlayer.tsx
@@ -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: `${newSentences[0]} ${newSentences[1]}
`,
+ lastUpdate: DateTime.now(),
+ })
+
+ beforeEach(jest.clearAllMocks)
+
+ const TestChild = ({ model }: { model?: PageModel }) => {
+ useTtsPlayer(model)
+ return null
+ }
+
+ const render = (model?: PageModel): RenderAPI =>
+ renderWithTheme(
+
+
+
+
+ ,
+ )
+
+ 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)
+ })
+})