diff --git a/tests/jest/plugins/tts/AbstractTTSEngine.test.js b/tests/jest/plugins/tts/AbstractTTSEngine.test.js index d35464196..2ad26a3bd 100644 --- a/tests/jest/plugins/tts/AbstractTTSEngine.test.js +++ b/tests/jest/plugins/tts/AbstractTTSEngine.test.js @@ -5,7 +5,7 @@ import PageChunkIterator from '@/src/plugins/tts/PageChunkIterator.js'; /** @typedef {import('@/src/plugins/tts/AbstractTTSEngine.js').TTSEngineOptions} TTSEngineOptions */ // Skipping because it's flaky. Fix in #672 -describe.skip('AbstractTTSEngine', () => { +describe('AbstractTTSEngine', () => { test('stops playing once done', async () => { class DummyEngine extends AbstractTTSEngine { getVoices() { return []; } @@ -20,6 +20,63 @@ describe.skip('AbstractTTSEngine', () => { }); }); +describe('Non Flaky AbstractTTSEngine', () => { + let d; + beforeEach(() => { + class DummyEngine extends AbstractTTSEngine { + getVoices() { return []; } + } + d = new DummyEngine(DUMMY_TTS_ENGINE_OPTS); + }); + + test('should trigger start event', () => { + const triggerStub = sinon.spy(d.events, 'trigger'); + d._chunkIterator = { next: sinon.stub().resolves(PageChunkIterator.AT_END) }; + d.start(0, 0); + expect(triggerStub.callCount).toBe(1); + }); + + test('should handle pause and resume events for state changes', () => { + const triggerStub = sinon.spy(d.events, 'trigger'); + d.activeSound = { pause: sinon.stub() }; + + d.paused = true; + d.pause(); + expect(triggerStub.callCount).toBe(0); + + d.paused = false; + d.pause(); + expect(triggerStub.callCount).toBe(1); + expect(d.paused).toBeTruthy(); + }); + + test('should trigger resume event', () => { + const triggerStub = sinon.spy(d.events, 'trigger'); + d.activeSound = { resume: sinon.stub() }; + + d.paused = true; + d.resume(); + expect(triggerStub.callCount).toBe(1); + expect(d.paused).toBeFalsy(); + }); + + test('togglePlayPause', () => { + const triggerSpy = sinon.spy(d.events, 'trigger'); + d.activeSound = { + resume: sinon.stub(), + pause: sinon.stub(), + }; + d.paused = true; + d.togglePlayPause(); + expect(d.paused).toBeFalsy(); + + d.togglePlayPause(); + expect(d.paused).toBeTruthy(); + expect(triggerSpy.callCount).toBe(2); + }); + +}); + for (const dummyVoice of [dummyVoiceHyphens, dummyVoiceUnderscores]) { describe(`getBestBookVoice with BCP47 ${dummyVoice == dummyVoiceUnderscores ? '+' : '-'} underscores`, () => { const { getBestBookVoice } = AbstractTTSEngine; diff --git a/tests/jest/plugins/tts/WebTTSEngine.test.js b/tests/jest/plugins/tts/WebTTSEngine.test.js index 6eeb0fb59..e35a496e4 100644 --- a/tests/jest/plugins/tts/WebTTSEngine.test.js +++ b/tests/jest/plugins/tts/WebTTSEngine.test.js @@ -15,11 +15,13 @@ beforeEach(() => { this.text = text; Object.assign(this, eventTargetMixin()); }; + // mockSomeConstantValueGetter.mockReturnValue(false); }); afterEach(() => { delete window.speechSynthesis; delete window.SpeechSynthesisUtterance; + // mockSomeConstantValueGetter.mockReset(); }); describe('WebTTSEngine', () => { @@ -92,6 +94,17 @@ describe('WebTTSSound', () => { }); }); + test('reloading should store the charIndex for the sound', async () => { + const sound = new WebTTSSound('hello world'); + sound.load(); + sound.play(); + sound.utterance.dispatchEvent('pause', {target: {text: 'hello world'}}); + await afterEventLoop(); + sound.reload(); + await afterEventLoop(); + expect(sound._charIndex).not.toBe(0); + }); + describe('_chromePausingBugFix', () => { /** @type {sinon.SinonFakeTimers} */ let clock = null; @@ -167,12 +180,69 @@ describe('WebTTSSound', () => { sound.started = true; sound.paused = true; const dispatchSpy = sinon.spy(sound.utterance, 'dispatchEvent'); + sound.play = sinon.stub(); sound.resume(); clock.tick(1000); + sound.stop = sinon.stub(); clock.restore(); await afterEventLoop(); expect(dispatchSpy.callCount).toBe(1); + expect(sound.play.callCount).toBe(1); expect(dispatchSpy.args[0][0].type).toBe('resume'); }); + + test('resume continues playing if not started or stopped', async() => { + const sound = new WebTTSSound('hello world'); + sound.load(); + sound.play = sinon.stub(); + sound.resume(); + await afterEventLoop(); + expect(sound.play.callCount).toBe(1); + }); + + test('fire finish event', async () => { + const sound = new WebTTSSound('hello world'); + sound.load(); + const dispatchSpy = sinon.spy(sound.utterance, 'dispatchEvent'); + sound.play(); + sound.stop = sinon.stub(); + sound.finish(); + await afterEventLoop(); + expect(dispatchSpy.args[0][0].type).toBe('finish'); + }); + + test('pause immediately fails if already in paused state', async() => { + const sound = new WebTTSSound('pausing on a pause'); + sound.load(); + sound.play(); + const checkSpeech = jest.spyOn(speechSynthesis, 'pause'); + sound.paused = true; + sound.pause(); + expect(checkSpeech).toHaveBeenCalledTimes(0); + }); +}); + +const mockSomeConstantValueGetter = jest.fn(); +jest.mock('../../../../src/plugins/tts/utils', () => ({ + ...jest.requireActual('../../../../src/plugins/tts/utils'), + get DEBUG_READ_ALOUD() { + return mockSomeConstantValueGetter; + }, +})); + +describe('DEBUG_READ_ALOUD', () => { + test('debug listeners should be added', async () => { + mockSomeConstantValueGetter.mockReturnValue(true); + const sound = new WebTTSSound('debug world'); + console.log = sinon.stub(); + expect(console.log.callCount).toBe(0); + sound.load(); + const EVENTS = ['pause', 'resume', 'start', 'end', 'error', 'boundary', 'mark', 'finish']; + + EVENTS.forEach((event) => { + sound.utterance.dispatchEvent(event); + }); + expect(console.log.callCount).toBe(EVENTS.length); + }); }); diff --git a/tests/jest/plugins/tts/utils.test.js b/tests/jest/plugins/tts/utils.test.js index 6892bd44c..be32894a4 100644 --- a/tests/jest/plugins/tts/utils.test.js +++ b/tests/jest/plugins/tts/utils.test.js @@ -40,6 +40,7 @@ describe('toISO6391', () => { expect(toISO6391('fr')).toBe('fr'); expect(toISO6391('SQ')).toBe('sq'); expect(toISO6391('aa')).toBe('aa'); + expect(toISO6391('zh-hans')).toBe('zh-hans'); }); test('ISO 639-2/T', () => {