// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
import type {CrToastElement} from '//resources/cr_elements/cr_toast/cr_toast.js';
import {BrowserProxy, ToolbarEvent} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
import type {AppElement} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
import {convertLangOrLocaleForVoicePackManager, VoiceClientSideStatusCode, VoicePackServerStatusErrorCode, VoicePackServerStatusSuccessCode} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
import {microtasksFinished} from 'chrome-untrusted://webui-test/test_util.js';
import {createAndSetVoices, createSpeechSynthesisVoice, emitEvent, setVoices} from './common.js';
import {FakeReadingMode} from './fake_reading_mode.js';
import {FakeSpeechSynthesis} from './fake_speech_synthesis.js';
import {TestColorUpdaterBrowserProxy} from './test_color_updater_browser_proxy.js';
suite('UpdateVoicePack', () => {
let app: AppElement;
let speechSynthesis: FakeSpeechSynthesis;
function setNaturalVoicesForLang(lang: string) {
createAndSetVoices(app, speechSynthesis, [
{lang: lang, name: 'Wall-e (Natural)'},
{lang: lang, name: 'Andy (Natural)'},
{lang: lang, name: 'Buzz'},
]);
}
setup(() => {
BrowserProxy.setInstance(new TestColorUpdaterBrowserProxy());
document.body.innerHTML = window.trustedTypes!.emptyHTML;
const readingMode = new FakeReadingMode();
chrome.readingMode = readingMode as unknown as typeof chrome.readingMode;
app = document.createElement('read-anything-app');
document.body.appendChild(app);
speechSynthesis = new FakeSpeechSynthesis();
app.synth = speechSynthesis;
app.getSpeechSynthesisVoice();
});
suite('updateVoicePackStatus', () => {
let sentInstallRequestFor: string = '';
suite('voice pack not installed', () => {
setup(() => {
chrome.readingMode.sendInstallVoicePackRequest = (lang) => {
sentInstallRequestFor = lang;
};
});
test('request install if we need to', () => {
const lang = 'it-it';
chrome.readingMode.isLanguagePackDownloadingEnabled = true;
chrome.readingMode.baseLanguageForSpeech = lang;
app.$.toolbar.updateFonts = () => {};
app.languageChanged();
const voicePackLang = convertLangOrLocaleForVoicePackManager(lang)!;
app.updateVoicePackStatus(voicePackLang, 'kNotInstalled');
const serverStatus =
app.getVoicePackStatusForTesting(voicePackLang).server;
assertEquals(
serverStatus.code, VoicePackServerStatusSuccessCode.NOT_INSTALLED);
assertEquals('Successful response', serverStatus.id);
assertEquals(voicePackLang, sentInstallRequestFor);
});
});
});
suite('download notification', () => {
let toast: CrToastElement;
setup(() => {
toast = app.shadowRoot!.querySelector<CrToastElement>('#toast')!;
app.getSpeechSynthesisVoice();
});
test('does not show if already installed', async () => {
const lang = 'en';
// The first call to update status should be the existing status from
// the server.
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
assertFalse(toast.open);
});
test('does not show if still installing', async () => {
const lang = 'en';
// existing status
app.updateVoicePackStatus(lang, 'kNotInstalled');
// then we request install
app.updateVoicePackStatus(lang, 'kInstalling');
await microtasksFinished();
assertFalse(toast.open);
});
test('does not show if error while installing', async () => {
const lang = 'en';
// existing status
app.updateVoicePackStatus(lang, 'kNotInstalled');
// then we request install
app.updateVoicePackStatus(lang, 'kInstalling');
// install error
app.updateVoicePackStatus(lang, 'kOther');
await microtasksFinished();
assertFalse(toast.open);
});
test('shows after installed', async () => {
const lang = 'en';
// existing status
app.updateVoicePackStatus(lang, 'kInstalled');
// then we request install
app.setVoicePackLocalStatus(
lang, VoiceClientSideStatusCode.SENT_INSTALL_REQUEST);
app.updateVoicePackStatus(lang, 'kInstalling');
// install completes
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
assertTrue(toast.open);
});
test('shows after installed with complete language locale', async () => {
const lang = 'ja';
// existing status
app.updateVoicePackStatus(lang, 'kNotInstalled');
// then we request install
app.updateVoicePackStatus(lang, 'kInstalling');
// install completes
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
assertTrue(toast.open);
assertTrue(
toast.querySelector('#toastTitle')!.textContent!.includes('ja-jp'));
});
});
test(
'unavailable even if natural voices are in the list for a different lang',
async () => {
const lang = 'fr';
setNaturalVoicesForLang('it');
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals('Successful response', status.server.id);
assertEquals(
status.client, VoiceClientSideStatusCode.INSTALLED_AND_UNAVAILABLE);
});
test(
'unavailable if non-natural voices are in the list for a different lang',
async () => {
const lang = 'de';
// Installed 'de' language pack, but the fake available voice list
// only has english voices.
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals(
status.client, VoiceClientSideStatusCode.INSTALLED_AND_UNAVAILABLE);
});
test(
'unavailable if only non-natural voices are in the list for this lang',
async () => {
const lang = 'en';
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals('Successful response', status.server.id);
assertEquals(
status.client, VoiceClientSideStatusCode.INSTALLED_AND_UNAVAILABLE);
});
test(
'available if natural voices are unsupported for this lang and voices are available',
async () => {
const lang = 'yue';
createAndSetVoices(app, speechSynthesis, [
{lang: 'yue-hk', name: 'Cantonese'},
]);
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals('Successful response', status.server.id);
assertEquals(status.client, VoiceClientSideStatusCode.AVAILABLE);
});
test(
'unavailable if natural voices are unsupported for this lang and voices unavailable',
async () => {
const lang = 'yue';
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals('Successful response', status.server.id);
assertEquals(
status.client, VoiceClientSideStatusCode.INSTALLED_AND_UNAVAILABLE);
});
test(
'available if natural voices are in installed for this lang',
async () => {
const lang = 'en-us';
// set installing status so that the old status is not empty.
app.updateVoicePackStatus(lang, 'kInstalling');
// set the voices on speech synthesis without triggering on voices
// changed, so we can verify that updateVoicePackStatus calls it.
speechSynthesis.setVoices([
createSpeechSynthesisVoice({lang: lang, name: 'Wall-e (Natural)'}),
createSpeechSynthesisVoice({lang: lang, name: 'Andy (Natural)'}),
]);
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(
status.server.code, VoicePackServerStatusSuccessCode.INSTALLED);
assertEquals('Successful response', status.server.id);
// This would be INSTALLED_AND_UNAVIALABLE if the voice list wasn't
// refreshed.
assertEquals(status.client, VoiceClientSideStatusCode.AVAILABLE);
});
test(
'with flag switches to newly available voices if it\'s for the current language',
async () => {
const lang = 'en-us';
chrome.readingMode.isLanguagePackDownloadingEnabled = true;
chrome.readingMode.isAutoVoiceSwitchingEnabled = true;
chrome.readingMode.baseLanguageForSpeech = lang;
app.enabledLangs = [lang];
chrome.readingMode.getStoredVoice = () => '';
setNaturalVoicesForLang(lang);
app.updateVoicePackStatus(lang, 'kInstalled');
await microtasksFinished();
const selectedVoice = app.getSpeechSynthesisVoice();
assertTrue(!!selectedVoice);
assertEquals(lang, selectedVoice.lang);
assertTrue(selectedVoice.name.includes('Natural'));
});
test(
'with flag does not switch to newly available voices if it\'s not for the current language',
() => {
const installedLang = 'en-us';
chrome.readingMode.isLanguagePackDownloadingEnabled = true;
chrome.readingMode.isAutoVoiceSwitchingEnabled = true;
chrome.readingMode.baseLanguageForSpeech = 'pt-br';
app.enabledLangs = [chrome.readingMode.baseLanguageForSpeech];
const currentVoice = createSpeechSynthesisVoice({
name: 'Portuguese voice 1',
lang: chrome.readingMode.baseLanguageForSpeech,
});
emitEvent(
app, ToolbarEvent.VOICE, {detail: {selectedVoice: currentVoice}});
chrome.readingMode.getStoredVoice = () => '';
setVoices(app, speechSynthesis, [currentVoice]);
app.updateVoicePackStatus(installedLang, 'kInstalled');
// The selected voice should stay the same as it was.
assertEquals(currentVoice, app.getSpeechSynthesisVoice());
});
test('with error code marks the status', () => {
const lang = 'en-us';
app.updateVoicePackStatus(lang, 'kOther');
const status = app.getVoicePackStatusForTesting(lang);
assertEquals(status.server.code, VoicePackServerStatusErrorCode.OTHER);
assertEquals('Unsuccessful response', status.server.id);
assertEquals(status.client, VoiceClientSideStatusCode.ERROR_INSTALLING);
});
suite('updateVoicePackStatusFromInstallResponse', () => {
suite('with error code', () => {
const lang = 'pt-br';
setup(() => {
app.enabledLangs.push(lang);
return microtasksFinished();
});
test('and no other voices for language, disables language', async () => {
createAndSetVoices(app, speechSynthesis, []);
app.updateVoicePackStatusFromInstallResponse(lang, 'kOther');
await microtasksFinished();
assertFalse(app.enabledLangs.includes(lang));
});
test(
'and only eSpeak voices for language, disables language',
async () => {
createAndSetVoices(app, speechSynthesis, [
{lang: lang, name: 'eSpeak Portuguese'},
]);
app.updateVoicePackStatusFromInstallResponse(lang, 'kOther');
await microtasksFinished();
assertFalse(app.enabledLangs.includes(lang));
});
test(
'and when language-pack lang does not match voice lang, ' +
'still disables language',
async () => {
app.enabledLangs.push('it-it');
createAndSetVoices(app, speechSynthesis, []);
app.updateVoicePackStatusFromInstallResponse('it', 'kOther');
await microtasksFinished();
assertFalse(app.enabledLangs.includes('it-it'));
});
test(
'and when language-pack lang does not match voice lang, with ' +
'e-speak voices, still disables language',
async () => {
app.enabledLangs.push('it-it');
createAndSetVoices(app, speechSynthesis, [
{lang: 'it', name: 'eSpeak Italian '},
]);
app.updateVoicePackStatusFromInstallResponse('it', 'kOther');
await microtasksFinished();
assertFalse(app.enabledLangs.includes('it-it'));
});
test(
'and has other Google voices for language, keeps language enabled',
async () => {
createAndSetVoices(app, speechSynthesis, [
{lang: lang, name: 'ChromeOS Portuguese 1'},
{lang: lang, name: 'ChromeOS Portuguese 2'},
]);
app.onVoicesChanged();
app.updateVoicePackStatusFromInstallResponse(lang, 'kOther');
await microtasksFinished();
assertTrue(app.enabledLangs.includes(lang));
});
});
});
});