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) + }) +})