improve tests, especially for navigation.

This commit is contained in:
Jordan
2025-02-27 08:23:27 -08:00
parent 6f941c56d1
commit 87446784ae
24 changed files with 748 additions and 448 deletions

View File

@ -1,13 +1,9 @@
import React, { useState, useEffect } from "react";
import { ScrollView, Text, TouchableHighlight, View } from "react-native";
import { ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native";
import { useNavigation, Route } from "@react-navigation/native";
import { Conversation, Message } from "@/app/lib/conversation";
import MessageBubble from "@/components/ui/MessageBubble";
import { WhisperContext } from "whisper.rn";
import { NavigationProp, ParamListBase } from "@react-navigation/native";
import { CachedTranslator, LanguageServer } from "@/app/i18n/api";
import { getDb } from "@/app/lib/db";
import LiveAudioStream from "react-native-live-audio-stream";
import { CachedTranslator, LanguageServer, language_matrix_entry } from "@/app/i18n/api";
const lasOptions = {
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
@ -35,18 +31,45 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
undefined | CachedTranslator
>();
const [languageLabels, setLanguageLabels] = useState<undefined | {
hostNative: {
host: string,
guest: string,
},
guestNative: {
host: string,
guest: string,
}
}>()
useEffect(() => {
(async () => {
setCachedTranslator(
new CachedTranslator(
"en",
conversation.guest.language,
await LanguageServer.getDefault()
)
);
if (!cachedTranslator) throw new Error("cachedTranslator is undefined");
setGuestSpeak(await cachedTranslator.translate("Speak"));
const languageServer = await LanguageServer.getDefault();
const languages = await languageServer.fetchLanguages();
const cc = new CachedTranslator(
"en",
conversation.guest.language,
languageServer,
)
setCachedTranslator(cc);
setGuestSpeak(await cc.translate("Speak"));
const hostLang1 = languages[conversation.host.language].name;
const guestLang1 = languages[conversation.host.language].name;
const hostLang2 = await cc.translate(languages[conversation.host.language].name);
const guestLang2 = await cc.translate(languages[conversation.host.language].name);
setLanguageLabels({
hostNative: {
host: hostLang1,
guest: guestLang1,
},
guestNative: {
host: hostLang2,
guest: guestLang2,
}
})
})();
const updateMessages = (c: Conversation) => {
setMessages([...c]);
};
@ -67,7 +90,11 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
return cachedTranslator ? (
<View style={{ flex: 1, flexDirection: "column" }}>
<Text>Conversation Thread</Text>
{languageLabels && (<View style={styles.languageLabels}>
<Text style={styles.nativeHostLabel}>{ languageLabels.hostNative.host } / { languageLabels.hostNative.guest }</Text>
<Text style={styles.nativeGuestLabel}>{ languageLabels.guestNative.host } / { languageLabels.guestNative.guest }</Text>
</View>)
}
<ScrollView
style={{
borderColor: "black",
@ -106,4 +133,16 @@ const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversa
);
};
const styles = StyleSheet.create({
languageLabels: {
},
nativeHostLabel: {
},
nativeGuestLabel: {
},
})
export default ConversationThread;

View File

@ -3,11 +3,11 @@ import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
import { useEffect, useState } from "react";
import ISpeakButton from "./ui/ISpeakButton";
import { LANG_FLAGS } from "@/app/i18n/lang";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { Conversation, Speaker } from "@/app/lib/conversation";
import { NavigationProp, ParamListBase } from "@react-navigation/native";
import { Link } from "expo-router";
import { Link, useNavigation } from "expo-router";
export function LanguageSelection(props: {
@ -17,9 +17,11 @@ export function LanguageSelection(props: {
}) {
const [languages, setLanguages] = useState<language_matrix | undefined>();
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
const [translator, setTranslator] = useState<Translator|undefined>();
const nav = useNavigation();
const languageServer = new LanguageServer(LIBRETRANSLATE_BASE_URL);
const translator = props.translator || new CachedTranslator("en", undefined, languageServer);
function onLangSelected(language: language_matrix_entry) {
props.onLangSelected && props.onLangSelected(language)
@ -27,33 +29,32 @@ export function LanguageSelection(props: {
useEffect(() => {
const fetchData = async () => {
(async () => {
try {
// Replace with your actual async data fetching logic
const languages = await languageServer.fetchLanguages();
setTranslator(await CachedTranslator.getDefault());
const languageServer = await LanguageServer.getDefault();
const languages = await languageServer.fetchLanguages(5000);
setLanguages(languages);
setLanguagesLoaded(true);
} catch (error) {
error = error as Response;
console.error('Error fetching data (%d %s): %s', (error as Response).status, (error as Response).statusText, (error as Response).body);
console.error('Error fetching languages from %s: %s', languageServer.baseUrl, error);
}
};
fetchData();
})();
}, []);
return (
<View>
<Link href={"/settings"}>
<Pressable onPress={() => nav.navigate('Settings')}>
<Text>Settings</Text>
</Link>
</Pressable>
<ScrollView >
<SafeAreaProvider >
<SafeAreaView>
{(languages && languagesLoaded) ? Object.entries(languages).filter((l) => (LANG_FLAGS as any)[l[0]] !== undefined).map(
([lang, lang_entry]) => {
return (
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} />
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} translator={translator} />
);
}
) : <Text>Waiting...</Text>

View File

@ -3,164 +3,280 @@ import React, { useState, useEffect } from "react";
import { View, Text, TextInput, StyleSheet, Pressable } from "react-native"; // Add Picker import
import { getDb } from "@/app/lib/db";
import { Settings } from "@/app/lib/settings";
import { LanguageServer } from "@/app/i18n/api";
import {Picker} from "@react-native-picker/picker"
import { LanguageServer, fetchWithTimeout } from "@/app/i18n/api";
import { Picker } from "@react-native-picker/picker";
import { longLang } from "@/app/i18n/lang";
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
import { WHISPER_MODELS, downloadWhisperModel, download_status, getWhisperDownloadStatus, whisper_model_tag_t } from "@/app/lib/whisper";
import {
WHISPER_MODELS,
WHISPER_MODEL_DIR,
downloadWhisperModel,
download_status,
getWhisperDownloadStatus,
getWhisperTarget,
whisper_model_tag_t,
} from "@/app/lib/whisper";
import { Paths } from "expo-file-system/next";
type Language = {
code: string;
name: string;
code: string;
name: string;
};
type LanguageMatrix = {
[key: string]: Language;
[key: string]: Language;
};
type connection_test_t =
| {
success: true;
}
| {
success: false;
error: string;
};
const SettingsComponent: React.FC = () => {
const [hostLanguage, setHostLanguage] = useState<string | null>(null);
const [libretranslateBaseUrl, setLibretranslateBaseUrl] = useState<string | null>(null);
const [languages, setLanguages] = useState<undefined|LanguageMatrix>();
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [whisperModel, setWhisperModel] = useState<undefined|whisper_model_tag_t>()
const [downloadStatus, setDownloadStatus] = useState<undefined|download_status>();
const [hostLanguage, setHostLanguage] = useState<string | null>(null);
const [libretranslateBaseUrl, setLibretranslateBaseUrl] = useState<
string | null
>(null);
const [languages, setLanguages] = useState<undefined | LanguageMatrix>();
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [whisperModel, setWhisperModel] = useState<
undefined | whisper_model_tag_t
>();
const [downloadStatus, setDownloadStatus] = useState<
undefined | download_status
>();
useEffect(() => {
(async () => {
// Fetch the database connection
const db = await getDb();
const settings = new Settings(db);
// Get the current settings values
const hostLang = await settings.getHostLanguage();
const libretranslateUrl = await settings.getLibretranslateBaseUrl();
const langServer = new LanguageServer(libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL);
const wModel = await settings.getWhisperModel()
const [langServerConn, setLangServerConn] = useState<
undefined | connection_test_t
>();
// Fetch languages from API
const langData = await langServer.fetchLanguages();
setLanguages(langData);
setHostLanguage(hostLang || "en");
setLibretranslateBaseUrl(libretranslateUrl);
setWhisperModel(wModel);
setIsLoaded(true);
})();
// Check for whether a model is currently downloading and set the status.
setInterval(async () => {
if (!whisperModel) return null;
const dlStatus = await getWhisperDownloadStatus(whisperModel);
setDownloadStatus(dlStatus)
}, 200);
}, []);
const doReadownload = async () => {
if (!whisperModel) return;
await downloadWhisperModel(whisperModel, {force_redownload: true});
}
const doDownload = async () => {
if (!whisperModel) return;
await downloadWhisperModel(whisperModel)
}
const handleHostLanguageChange = async (value: string) => {
setHostLanguage(value);
// Fetch the database connection
const db = await getDb();
const settings = new Settings(db);
// Save the updated setting value
await settings.setHostLanguage(value);
};
const handleLibretranslateBaseUrlChange = async (value: string) => {
setLibretranslateBaseUrl(value);
// Fetch the database connection
const db = await getDb();
const settings = new Settings(db);
// Save the updated setting value
await settings.setLibretranslateBaseUrl(value);
};
return (
isLoaded ? <View style={styles.container}>
<Text style={styles.label}>Host Language:</Text>
<Picker
selectedValue={hostLanguage || ""}
style={{ height: 50, width: "100%" }}
onValueChange={handleHostLanguageChange}
accessibilityHint="language"
>
{languages && Object.keys(languages).map((langCode) => (
<Picker.Item key={langCode} label={longLang(langCode)} value={langCode} />
))}
</Picker>
<Text style={styles.label}>LibreTranslate Base URL:</Text>
<TextInput
style={styles.input}
value={libretranslateBaseUrl || ""}
onChangeText={handleLibretranslateBaseUrlChange}
accessibilityHint="libretranslate base url"
/>
<Picker
selectedValue={whisperModel || ""}
style={{ height: 50, width: "100%" }}
onValueChange={setWhisperModel}
accessibilityHint="language"
>
{Object.entries(WHISPER_MODELS).map(([key, {label}]) => (
<Picker.Item key={key} label={label} value={key} />
))}
</Picker>
{whisperModel && (
<View>
{
downloadStatus?.status === "complete" ? (
<Pressable onPress={doReadownload}>
Re-Download
</Pressable>
) : (
downloadStatus?.status === "in_progress" ? (
<Text>
{downloadStatus.bytes.done / downloadStatus.bytes.total * 100.0} % complete
{ downloadStatus.bytes.done } bytes of { downloadStatus.bytes.total }
</Text>
) : (
<Pressable onPress={doDownload}>
Download
</Pressable>
)
)
}
</View>
)}
</View> : <View><Text>Loading ...</Text></View>
const fillHostLanguageOptions = async () => {
const settings = await Settings.getDefault();
const hostLang = await settings.getHostLanguage();
setHostLanguage(hostLang || "en");
const langServer = new LanguageServer(
libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL
);
// Fetch languages from API
try {
const langData = await langServer.fetchLanguages();
setLanguages(langData);
setLangServerConn({ success: true });
} catch (err) {
console.warn("Got an error fetching: %s", err);
setLangServerConn({
success: false,
error: `Could not connect to ${libretranslateBaseUrl}: ${err}`,
});
}
}
useEffect(() => {
(async () => {
// Fetch the database connection
// const db = await getDb("down");
const settings = await Settings.getDefault();
await fillHostLanguageOptions();
console.log("Fetched settings");
// Get the current settings values
const libretranslateUrl =
(await settings.getLibretranslateBaseUrl()) || LIBRETRANSLATE_BASE_URL;
setLibretranslateBaseUrl(libretranslateUrl);
console.log("libretranslate url = %s", libretranslateUrl);
try {
const wModel = await settings.getWhisperModel();
setWhisperModel(wModel || "small");
} catch (err) {
console.warn(err);
}
// setWhisperModel(wModel);
setIsLoaded(true);
// console.log("Set is loaded: %s", isLoaded);
})();
// Check for whether a model is currently downloading and set the status.
setInterval(async () => {
if (!whisperModel) return null;
const dlStatus = await getWhisperDownloadStatus(whisperModel);
setDownloadStatus(dlStatus);
}, 200);
setInterval(async () => {
if (!libretranslateBaseUrl) return;
try {
const resp = await fetchWithTimeout(
libretranslateBaseUrl + "/languages",
{
method: "HEAD",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
},
5000
);
if (resp.status !== 200) {
throw new Error(resp.statusText);
}
setLangServerConn({ success: true });
} catch (err) {
setLangServerConn({
success: false,
error: `Could not connect to ${libretranslateBaseUrl}: ${err}`,
});
}
}, 1000);
setInterval(async () => {
const settings = await Settings.getDefault();
await settings.setHostLanguage(hostLanguage || "en");
await settings.setLibretranslateBaseUrl(
libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL
);
await settings.setWhisperModel(whisperModel || "small");
}, 1000);
}, []);
const doReadownload = async () => {
if (!whisperModel) return;
await downloadWhisperModel(whisperModel, { force_redownload: true });
};
const doDownload = async () => {
if (!whisperModel) return;
await downloadWhisperModel(whisperModel);
};
const handleHostLanguageChange = async (value: string) => {
setHostLanguage(value);
// Fetch the database connection
const db = await getDb();
const settings = new Settings(db);
// Save the updated setting value
await settings.setHostLanguage(value);
};
const handleLibretranslateBaseUrlChange = async (value: string) => {
setLibretranslateBaseUrl(value);
const settings = await Settings.getDefault();
// Save the updated setting value
await settings.setLibretranslateBaseUrl(value);
await fillHostLanguageOptions();
};
return isLoaded ? (
<View style={styles.container}>
<Text style={styles.label}>Host Language:</Text>
<Picker
selectedValue={hostLanguage || ""}
style={{ height: 50, width: "100%" }}
onValueChange={handleHostLanguageChange}
accessibilityHint="hostLanguage"
>
{languages &&
Object.entries(languages).map((lang) => (
<Picker.Item key={lang[0]} label={lang[1].name} value={lang[0]} />
))}
</Picker>
<Text style={styles.label}>LibreTranslate Base URL:</Text>
<TextInput
style={styles.input}
value={libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL}
onChangeText={handleLibretranslateBaseUrlChange}
accessibilityHint="libretranslate base url"
/>
{langServerConn &&
(langServerConn.success ? (
<Text>Success connecting to {libretranslateBaseUrl}</Text>
) : (
<Text>
Error connecting to {libretranslateBaseUrl}: {langServerConn.error}
</Text>
))}
<Picker
selectedValue={whisperModel || ""}
style={{ height: 50, width: "100%" }}
onValueChange={setWhisperModel}
accessibilityHint="language"
>
{Object.entries(WHISPER_MODELS).map(([key, { label }]) => (
<Picker.Item key={key} label={label} value={key} />
))}
</Picker>
{whisperModel && (
<View>
{downloadStatus?.status === "complete" ? (
<Pressable onPress={doReadownload}>Re-Download</Pressable>
) : downloadStatus?.status === "in_progress" ? (
<Text>
{(downloadStatus.bytes.done / downloadStatus.bytes.total) * 100.0}{" "}
% complete
{downloadStatus.bytes.done} bytes of {downloadStatus.bytes.total}
</Text>
) : (
<View>
<Pressable onPress={doDownload} style={styles.button}>
<Text style={styles.buttonText}>Download</Text>
</Pressable>
<Text>
This will download to {Paths.join(WHISPER_MODEL_DIR, WHISPER_MODELS[whisperModel].target)}
</Text>
</View>
)}
</View>
)}
</View>
) : (
<View>
<Text>Loading ...</Text>
</View>
);
};
// Create styles for the component
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
label: {
fontSize: 16,
marginBottom: 8,
},
input: {
height: 40,
borderColor: "gray",
borderWidth: 1,
marginBottom: 20,
paddingHorizontal: 8,
},
button: {
backgroundColor: "blue",
flexDirection: "row",
display: "flex",
flexShrink: 1,
padding: 20,
alignItems: "center",
alignContent: "center",
},
buttonText: {
color: "white",
alignSelf: "center",
},
container: {
flex: 1,
padding: 20,
},
label: {
fontSize: 16,
marginBottom: 8,
},
input: {
height: 40,
borderColor: "gray",
borderWidth: 1,
marginBottom: 20,
paddingHorizontal: 8,
},
});
export default SettingsComponent;
export default SettingsComponent;

View File

@ -1,42 +1,42 @@
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import SettingsComponent from '@/components/Settings';
import { LanguageSelection } from '@/components/LanguageSelection';
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
import * as React from "react";
import SettingsComponent from "@/components/Settings";
import { LanguageSelection } from "@/components/LanguageSelection";
import {
useNavigation,
} from '@react-navigation/native'
import ConversationThread from '@/components/ConversationThread';
import { language_matrix_entry, Translator } from '@/app/i18n/api';
import { useRouter } from 'expo-router';
import { Conversation } from '@/app/lib/conversation';
import { Settings } from '@/app/lib/settings';
import { RootStackParamList } from '@/navigation.types';
createNativeStackNavigator,
NativeStackNavigationProp,
} from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native";
import ConversationThread from "@/components/ConversationThread";
import { language_matrix_entry, Translator } from "@/app/i18n/api";
import { Conversation } from "@/app/lib/conversation";
import { Settings } from "@/app/lib/settings";
import { RootStackParamList } from "@/navigation.types";
const Stack = createNativeStackNavigator();
export default function TTNavStack() {
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'ConversationThread'>>();
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, "Conversation">>()
async function onLangSelected(lang: language_matrix_entry) {
const settings = await Settings.getDefault();
const hostLanguage = await settings.getHostLanguage();
const conversation = new Conversation(
(await Translator.getDefault(lang.code)),
await Translator.getDefault(lang.code),
{ id: "host", language: hostLanguage },
{ "id": "guest", language: lang.code, }
)
nav.navigate("Conversation", { conversation, })
{ id: "guest", language: lang.code }
);
nav.navigate("ConversationThread", { conversation });
}
return (
<Stack.Navigator initialRouteName='LanguageSelection'>
<Stack.Screen name="LanguageSelection" >
{ props => <LanguageSelection {...props} onLangSelected={(l) => onLangSelected(l)} />}
</Stack.Screen>
<Stack.Screen name="ConversationThread" component={ConversationThread} />
<Stack.Screen name="Settings" component={SettingsComponent} />
</Stack.Navigator>
<Stack.Navigator initialRouteName="LanguageSelection">
<Stack.Screen name="LanguageSelection">
{(props) => (
<LanguageSelection {...props} onLangSelected={onLangSelected} />
)}
</Stack.Screen>
<Stack.Screen name="ConversationThread" component={ConversationThread} />
<Stack.Screen name="Settings" component={SettingsComponent} />
</Stack.Navigator>
);
}
}

View File

@ -1,35 +1,61 @@
import {dirname, resolve} from 'path'
import React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import { createStackNavigator } from '@react-navigation/stack';
jest.mock("@/app/i18n/api", () => require("../../__mocks__/api.ts"));
import { renderRouter} from 'expo-router/testing-library';
import React from "react";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react-native";
import {
NavigationContainer,
createNavigationContainerRef,
} from "@react-navigation/native";
import TTNavStack from "../TTNavStack";
import TTNavStack from '../TTNavStack';
const Stack = createStackNavigator();
describe('Navigation', () => {
describe("Navigation", () => {
beforeEach(() => {
// Reset the navigation state before each test
jest.clearAllMocks();
jest.useFakeTimers();
});
it('Navigates to ConversationThread on language selection', async () => {
render(<TTNavStack />);
const languageSelectionText = await screen.findByText(/I Speak French\./i);
it("Navigates to ConversationThread on language selection", async () => {
const MockComponent = jest.fn(() => <TTNavStack />);
renderRouter(
{
index: MockComponent,
},
{
initialUrl: '/',
}
);
const languageSelectionText = await waitFor(() =>
screen.getByText(/.*I Speak French.*/i)
);
act(() => {
fireEvent.press(languageSelectionText);
})
});
expect(await screen.findByText("Conversation Thread")).toBeOnTheScreen();
});
it('Navigates to Settings on settings selection', async () => {
render(<TTNavStack />);
const settingsButton = await screen.findByText("Settings");
act(() => {
fireEvent.press(settingsButton)
})
expect(await screen.findByText("Settings")).toBeOnTheScreen();
it("Navigates to Settings on settings selection", async () => {
const MockComponent = jest.fn(() => <TTNavStack />);
renderRouter(
{
index: MockComponent,
},
{
initialUrl: '/',
}
);
const settingsButton = await waitFor(() =>
screen.getByText(/.*Settings.*/i)
);
fireEvent.press(settingsButton);
expect(await waitFor(() => screen.getByText(/Settings/i))).toBeOnTheScreen();
// expect(waitFor(() => screen.getByText(/Settings/i))).toBeTruthy()
expect(screen.getByText("Settings")).toBeOnTheScreen();
});
});
});

View File

@ -1,121 +1,142 @@
// import AsyncStorage from '@react-native-async-storage/async-storage';
import { CachedTranslator, Translator, language_matrix_entry } from "@/app/i18n/api"
import { longLang } from "@/app/i18n/lang"
import React, { useEffect, useRef, useState } from "react"
import { Button, Image, ImageBackground, Pressable, StyleSheet, TouchableOpacity, View } from "react-native"
import { Text } from 'react-native';
import {
CachedTranslator,
Translator,
language_matrix_entry,
} from "@/app/i18n/api";
import { longLang } from "@/app/i18n/lang";
import React, { useEffect, useRef, useState } from "react";
import {
Button,
Image,
ImageBackground,
Pressable,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { Text } from "react-native";
import CountryFlag from "react-native-country-flag";
import { chooseCountry } from '@/app/i18n/countries';
import { chooseCountry } from "@/app/i18n/countries";
type ISpeakButtonProps = {
language: language_matrix_entry,
translator?: Translator,
onLangSelected?: (lang : language_matrix_entry) => any | Promise<any>,
language: language_matrix_entry;
translator?: Translator;
onLangSelected?: (lang: language_matrix_entry) => any | Promise<any>;
};
function iSpeak(language: language_matrix_entry) {
return `I speak ${language.name}.`;
}
function iSpeak(language : language_matrix_entry) {
return `I speak ${language.name}.`
}
async function iSpeakTr(translator : CachedTranslator, targetLang : language_matrix_entry) {
const sourceStr = iSpeak(targetLang)
return await translator.translate(sourceStr, targetLang.code);
async function iSpeakTr(
translator: CachedTranslator,
targetLang: language_matrix_entry
) {
const sourceStr = iSpeak(targetLang);
return await translator.translate(sourceStr, targetLang.code);
}
const DEFAULT_FLAGS = {
"en": ["us", "gb"],
// "sq": ["al"],
"ar": ["ae"],
"es": ["es"],
"pt": ["pt"],
"ru": ["ru"],
"it": ["it"],
"ir": ["ie"],
"sk": ["sk"],
"ro": ["ro"],
"ja": ["jp"],
"ko": ["kp", "kr"],
"el": ["gr"],
"fr": ["fr"],
"de": ["de"],
"nl": ["nl"],
"cz": ["cz"],
"uk": ["ua"],
"he": ["il"],
"hi": ["in"],
"gl": ["es"],
"fa": ["ir"],
"ur": ["pk"],
"ga": ["ie"],
"eo": ["es"]
}
en: ["us", "gb"],
// "sq": ["al"],
ar: ["ae"],
es: ["es"],
pt: ["pt"],
ru: ["ru"],
it: ["it"],
ir: ["ie"],
sk: ["sk"],
ro: ["ro"],
ja: ["jp"],
ko: ["kp", "kr"],
el: ["gr"],
fr: ["fr"],
de: ["de"],
nl: ["nl"],
cz: ["cz"],
uk: ["ua"],
he: ["il"],
hi: ["in"],
gl: ["es"],
fa: ["ir"],
ur: ["pk"],
ga: ["ie"],
eo: ["es"],
};
const ISpeakButton = (props : ISpeakButtonProps) => {
const ISpeakButton = (props: ISpeakButtonProps) => {
const [title, setTitle] = useState<string | undefined>();
const [titleLoaded, setTitleLoaded] = useState<boolean>(false);
const [translator, setTranslator] = useState<Translator | undefined>(
undefined
);
const [title, setTitle] = useState<string | undefined>();
const [titleLoaded, setTitleLoaded] = useState<boolean>(false);
const translator = props.translator || new CachedTranslator("en");
useEffect(() => {
(async function () {
const tr = props.translator || (await CachedTranslator.getDefault());
if (!tr) {
console.error("Failed to construct cachedTranslator");
}
setTranslator(tr);
try {
// Replace with your actual async data fetching logic
const title2 = await iSpeakTr(tr, props.language);
setTitle(title2);
} catch (error) {
console.error("Error fetching data from %s: %s", tr.languageServer.baseUrl, error);
} finally {
setTitleLoaded(true);
}
})();
}, []);
useEffect(() => {
const fetchData = async () => {
try {
// Replace with your actual async data fetching logic
const title = await iSpeakTr(translator, props.language);
setTitle(title);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setTitleLoaded(true);
}
};
fetchData();
}, []);
const countries = DEFAULT_FLAGS[props.language.code] || chooseCountry(props.language.code);
return (
title ? (
<TouchableOpacity style={styles.button} onPress={() => props.onLangSelected && props.onLangSelected(props.language)}>
<View>
<View style={styles.flag}>
{countries &&
countries.map( c => {
return <CountryFlag isoCode={c} size={25} key={c}/> }
)
}
</View>
<View style={styles.iSpeak}>
<Text style={styles.iSpeakText}>{ title }</Text>
</View>
</View>
</TouchableOpacity>
) : (
<Text>Loading...</Text>
)
)
const countries =
// @ts-ignore
DEFAULT_FLAGS[props.language.code] || chooseCountry(props.language.code);
}
return title ? (
<TouchableOpacity
style={styles.button}
onPress={() =>
props.onLangSelected && props.onLangSelected(props.language)
}
>
<View>
<View style={styles.flag}>
{countries &&
countries.map((c) => {
return <CountryFlag isoCode={c} size={25} key={c} />;
})}
</View>
<View>
<Text style={styles.iSpeakText}>{title}</Text>
</View>
</View>
</TouchableOpacity>
) : (
<Text>Loading...</Text>
);
};
const styles = StyleSheet.create({
button: {
width: "20%",
borderRadius: 10,
borderColor: "white",
borderWidth: 1,
borderStyle: "solid",
height: 110,
alignSelf: "flex-start",
margin: 8,
},
flag: {
},
iSpeak: {
textAlign: "center",
},
iSpeakText: {
textAlign: "center"
}
})
button: {
width: "20%",
borderRadius: 10,
borderColor: "white",
borderWidth: 1,
borderStyle: "solid",
height: 110,
alignSelf: "flex-start",
margin: 8,
},
flag: {},
iSpeak: {
textAlign: "center",
},
iSpeakText: {
textAlign: "center",
},
});
export default ISpeakButton;
export default ISpeakButton;

View File

@ -1,9 +1,9 @@
import React, { Dispatch } from "react";
import { render, screen, fireEvent, act } from "@testing-library/react-native";
import SettingsComponent from "@/components/Settings";
import { Settings } from "@/app/lib/settings";
import { getDb } from "@/app/lib/db";
import { language_matrix } from "@/app/i18n/api";
import { Settings } from "@/app/lib/settings";
import { getDb, migrateDb } from "@/app/lib/db";
const RENDER_TIME = 1000;
@ -49,45 +49,10 @@ jest.mock("@/app/i18n/api", () => {
}
})
jest.mock("@/app/lib/db", () => {
return {
getDb: jest.fn(() => {
return {
runAsync: jest.fn((statement : string, value : string) => {}),
getFirstAsync: jest.fn((statement : string, value : string) => {
return []
}),
}
})
}
})
jest.mock("@/app/lib/settings", () => {
const originalModule = jest.requireActual('@/app/lib/settings');
class MockSettings {
public constructor(public db = {}) {}
public setHostLanguage = jest.fn((val : string) => {
})
public setLibretranslateBaseUrl(val : string) {
}
getHostLanguage = jest.fn(() => {
return "en"
})
getLibretranslateBaseUrl = jest.fn(() => {
return "http://localhost:5004"
});
}
return {
...originalModule,
Settings: MockSettings
}
})
describe("SettingsComponent", () => {
beforeEach(async() => {
const settings = new Settings(await getDb());
await migrateDb();
const settings = await Settings.getDefault();
await settings.setHostLanguage("en");
await settings.setLibretranslateBaseUrl("https://example.com");
})
@ -116,13 +81,12 @@ describe("SettingsComponent", () => {
test("updates host language setting when input changes", async () => {
render(<SettingsComponent />);
// Wait for the component to fetch and display the initial settings
await screen.findByText(/Host Language:/i);
await screen.findByText(/LibreTranslate Base URL:/i);
// Change the host language input value
const picker = screen.getByAccessibilityHint("language");
const picker = screen.getByAccessibilityHint("hostLanguage");
fireEvent(picker, "onvalueChange", "es");
expect(picker.props.selectedIndex).toStrictEqual(0);
});