Add manual translate as default (#33)
* Added translate and switch auto buttons * Tests updated * Added hotkey & improved buttons visually
This commit is contained in:
@@ -13,7 +13,7 @@ Alternative front-end for Google Translate, serving as a Free and Open Source tr
|
|||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
|
||||||
Inspired by projects like [NewPipe](https://github.com/TeamNewPipe/NewPipe), [Nitter](https://github.com/zedeus/nitter), [Invidious](https://github.com/iv-org/invidious) or [Bibliogram](https://git.sr.ht/~cadence/bibliogram), *Lingva* scrappes through GTranslate and retrieves the translation without using any Google-related service, preventing them from tracking.
|
Inspired by projects like [NewPipe](https://github.com/TeamNewPipe/NewPipe), [Nitter](https://github.com/zedeus/nitter), [Invidious](https://github.com/iv-org/invidious) or [Bibliogram](https://git.sr.ht/~cadence/bibliogram), *Lingva* scrapes through GTranslate and retrieves the translation without using any Google-related service, preventing them from tracking.
|
||||||
|
|
||||||
For this purpose, *Lingva* is built, among others, with the following Open Source resources:
|
For this purpose, *Lingva* is built, among others, with the following Open Source resources:
|
||||||
|
|
||||||
|
|||||||
38
components/AutoTranslateButton.tsx
Normal file
38
components/AutoTranslateButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useState, useEffect, FC } from "react";
|
||||||
|
import { IconButton } from "@chakra-ui/react";
|
||||||
|
import { FaBolt } from "react-icons/fa";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onAuto: () => void,
|
||||||
|
[key: string]: any
|
||||||
|
};
|
||||||
|
|
||||||
|
const initLocalStorage = () => {
|
||||||
|
const initial = typeof window !== "undefined" && localStorage.getItem("isauto");
|
||||||
|
return initial ? initial === "true" : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AutoTranslateButton: FC<Props> = ({ onAuto, ...props }) => {
|
||||||
|
const [isAuto, setIsAuto] = useState(initLocalStorage);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("isauto", isAuto.toString());
|
||||||
|
}, [isAuto]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isAuto && onAuto();
|
||||||
|
}, [isAuto, onAuto]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Switch auto"
|
||||||
|
icon={<FaBolt />}
|
||||||
|
colorScheme="lingva"
|
||||||
|
variant={isAuto ? "solid" : "outline"}
|
||||||
|
onClick={() => setIsAuto(current => !current)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoTranslateButton;
|
||||||
@@ -13,7 +13,7 @@ const LangSelect: FC<Props> = ({ value, onChange, langs, ...props }) => (
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
variant="flushed"
|
variant="flushed"
|
||||||
px={2}
|
px={3}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
style={{ textAlignLast: "center" }}
|
style={{ textAlignLast: "center" }}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { FC, ChangeEvent } from "react";
|
import { FC } from "react";
|
||||||
import { Box, HStack, Textarea, IconButton, Tooltip, Spinner, useBreakpointValue, useColorModeValue, useClipboard } from "@chakra-ui/react";
|
import { Box, HStack, Textarea, IconButton, Tooltip, Spinner, TextareaProps, useBreakpointValue, useColorModeValue, useClipboard } from "@chakra-ui/react";
|
||||||
import { FaCopy, FaCheck, FaPlay, FaStop } from "react-icons/fa";
|
import { FaCopy, FaCheck, FaPlay, FaStop } from "react-icons/fa";
|
||||||
import { useAudioFromBuffer } from "@hooks";
|
import { useAudioFromBuffer } from "@hooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string,
|
value: string,
|
||||||
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void,
|
onChange?: TextareaProps["onChange"],
|
||||||
|
onSubmit?: () => void,
|
||||||
readOnly?: true,
|
readOnly?: true,
|
||||||
audio?: number[],
|
audio?: number[],
|
||||||
canCopy?: boolean,
|
canCopy?: boolean,
|
||||||
@@ -13,7 +14,7 @@ type Props = {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
};
|
};
|
||||||
|
|
||||||
const TranslationArea: FC<Props> = ({ value, onChange, readOnly, audio, canCopy, isLoading, ...props }) => {
|
const TranslationArea: FC<Props> = ({ value, onChange, onSubmit, readOnly, audio, canCopy, isLoading, ...props }) => {
|
||||||
const { hasCopied, onCopy } = useClipboard(value);
|
const { hasCopied, onCopy } = useClipboard(value);
|
||||||
const { audioExists, isAudioPlaying, onAudioClick } = useAudioFromBuffer(audio);
|
const { audioExists, isAudioPlaying, onAudioClick } = useAudioFromBuffer(audio);
|
||||||
const spinnerProps = {
|
const spinnerProps = {
|
||||||
@@ -36,6 +37,7 @@ const TranslationArea: FC<Props> = ({ value, onChange, readOnly, audio, canCopy,
|
|||||||
rows={useBreakpointValue([6, null, 12]) ?? undefined}
|
rows={useBreakpointValue([6, null, 12]) ?? undefined}
|
||||||
size="lg"
|
size="lg"
|
||||||
data-gramm_editor={false}
|
data-gramm_editor={false}
|
||||||
|
onKeyPress={e => (e.ctrlKey || e.metaKey) && e.key === "Enter" && onSubmit?.()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<HStack
|
<HStack
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export { default as Footer } from "./Footer";
|
|||||||
export { default as ColorModeToggler } from "./ColorModeToggler";
|
export { default as ColorModeToggler } from "./ColorModeToggler";
|
||||||
export { default as LangSelect } from "./LangSelect";
|
export { default as LangSelect } from "./LangSelect";
|
||||||
export { default as TranslationArea } from "./TranslationArea";
|
export { default as TranslationArea } from "./TranslationArea";
|
||||||
|
export { default as AutoTranslateButton } from "./AutoTranslateButton";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import faker from "faker";
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit("/");
|
cy.visit("/");
|
||||||
|
cy.clearLocalStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches page on inputs change & goes back correctly", () => {
|
it("switches page on inputs change & goes back correctly", () => {
|
||||||
@@ -11,13 +12,16 @@ it("switches page on inputs change & goes back correctly", () => {
|
|||||||
cy.findByRole("textbox", { name: /translation query/i })
|
cy.findByRole("textbox", { name: /translation query/i })
|
||||||
.as("query")
|
.as("query")
|
||||||
.type("palabra");
|
.type("palabra");
|
||||||
cy.findByText(/loading translation/i)
|
cy.findByRole("button", { name: /translate/i })
|
||||||
.should("be.visible");
|
.click();
|
||||||
cy.findByRole("textbox", { name: /translation result/i })
|
cy.findByRole("textbox", { name: /translation result/i })
|
||||||
.as("translation")
|
.as("translation")
|
||||||
.should("have.value", "word")
|
.should("have.value", "word")
|
||||||
.url()
|
.url()
|
||||||
.should("include", "/auto/en/palabra");
|
.should("include", "/auto/en/palabra");
|
||||||
|
cy.findByRole("button", { name: /switch auto/i })
|
||||||
|
.click();
|
||||||
|
|
||||||
// source change
|
// source change
|
||||||
cy.findByRole("combobox", { name: /source language/i })
|
cy.findByRole("combobox", { name: /source language/i })
|
||||||
.as("source")
|
.as("source")
|
||||||
@@ -71,6 +75,9 @@ it("switches first loaded page and back and forth on language change", () => {
|
|||||||
const query = faker.random.words();
|
const query = faker.random.words();
|
||||||
cy.visit(`/auto/en/${query}`);
|
cy.visit(`/auto/en/${query}`);
|
||||||
|
|
||||||
|
cy.findByRole("button", { name: /switch auto/i })
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.findByRole("textbox", { name: /translation query/i })
|
cy.findByRole("textbox", { name: /translation query/i })
|
||||||
.as("query")
|
.as("query")
|
||||||
.should("have.value", query);
|
.should("have.value", query);
|
||||||
@@ -92,6 +99,9 @@ it("switches first loaded page and back and forth on language change", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("language switching button is disabled on 'auto', but enables when other", () => {
|
it("language switching button is disabled on 'auto', but enables when other", () => {
|
||||||
|
cy.findByRole("button", { name: /switch auto/i })
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.findByRole("button", { name: /switch languages/i })
|
cy.findByRole("button", { name: /switch languages/i })
|
||||||
.as("btnSwitch")
|
.as("btnSwitch")
|
||||||
.should("be.disabled");
|
.should("be.disabled");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^@(components|hooks|pages|public|tests|utils|theme)(.*)$": "<rootDir>/$1$2"
|
"^@(components|hooks|mocks|pages|public|tests|utils|theme)(.*)$": "<rootDir>/$1$2"
|
||||||
},
|
},
|
||||||
testEnvironment: "jsdom"
|
testEnvironment: "jsdom"
|
||||||
}
|
}
|
||||||
|
|||||||
9
mocks/localStorage.ts
Normal file
9
mocks/localStorage.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Object.defineProperty(window, "localStorage", {
|
||||||
|
value: {
|
||||||
|
getItem: jest.fn(() => null),
|
||||||
|
setItem: jest.fn(() => null)
|
||||||
|
},
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const localStorageSetMock = window.localStorage.setItem;
|
||||||
3
mocks/next.ts
Normal file
3
mocks/next.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Router from "next/router";
|
||||||
|
|
||||||
|
export const routerPushMock = jest.spyOn(Router, "push").mockImplementation(async () => true);
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useReducer, FC, ChangeEvent } from "react";
|
import { useEffect, useReducer, useCallback, FC, ChangeEvent } from "react";
|
||||||
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||||
import Router from "next/router";
|
import Router from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react";
|
import { Stack, VStack, HStack, IconButton } from "@chakra-ui/react";
|
||||||
import { FaExchangeAlt } from "react-icons/fa";
|
import { FaExchangeAlt } from "react-icons/fa";
|
||||||
|
import { HiTranslate } from "react-icons/hi";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { CustomHead, LangSelect, TranslationArea } from "@components";
|
import { CustomHead, LangSelect, TranslationArea } from "@components";
|
||||||
import { useToastOnLoad } from "@hooks";
|
import { useToastOnLoad } from "@hooks";
|
||||||
@@ -10,6 +12,8 @@ import { googleScrape, extractSlug, textToSpeechScrape } from "@utils/translate"
|
|||||||
import { retrieveFiltered, replaceBoth } from "@utils/language";
|
import { retrieveFiltered, replaceBoth } from "@utils/language";
|
||||||
import langReducer, { Actions, initialState } from "@utils/reducer";
|
import langReducer, { Actions, initialState } from "@utils/reducer";
|
||||||
|
|
||||||
|
const AutoTranslateButton = dynamic(() => import("@components/AutoTranslateButton"), { ssr: false });
|
||||||
|
|
||||||
const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, translationRes, audio, errorMsg, initial }) => {
|
const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, translationRes, audio, errorMsg, initial }) => {
|
||||||
const [{ source, target, query, delayedQuery, translation, isLoading }, dispatch] = useReducer(langReducer, initialState);
|
const [{ source, target, query, delayedQuery, translation, isLoading }, dispatch] = useReducer(langReducer, initialState);
|
||||||
|
|
||||||
@@ -23,6 +27,20 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeRoute = useCallback((customQuery: string) => {
|
||||||
|
if (isLoading)
|
||||||
|
return;
|
||||||
|
if (!customQuery || customQuery === initialState.query)
|
||||||
|
return;
|
||||||
|
if (!home && !initial)
|
||||||
|
return;
|
||||||
|
if (!home && customQuery === initial.query && source === initial.source && target === initial.target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
||||||
|
Router.push(`/${source}/${target}/${encodeURIComponent(customQuery)}`);
|
||||||
|
}, [isLoading, source, target, home, initial]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (home)
|
if (home)
|
||||||
return dispatch({ type: Actions.SET_ALL, payload: { state: { ...initialState, isLoading: false } } });
|
return dispatch({ type: Actions.SET_ALL, payload: { state: { ...initialState, isLoading: false } } });
|
||||||
@@ -42,20 +60,6 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
|||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoading)
|
|
||||||
return;
|
|
||||||
if (!delayedQuery || delayedQuery === initialState.query)
|
|
||||||
return;
|
|
||||||
if (!home && !initial)
|
|
||||||
return;
|
|
||||||
if (!home && delayedQuery === initial.query && source === initial.source && target === initial.target)
|
|
||||||
return;
|
|
||||||
|
|
||||||
dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
|
||||||
Router.push(`/${source}/${target}/${encodeURIComponent(delayedQuery)}`);
|
|
||||||
}, [source, target, delayedQuery, initial, home, isLoading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (url: string) => url === Router.asPath || dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
const handler = (url: string) => url === Router.asPath || dispatch({ type: Actions.SET_FIELD, payload: { key: "isLoading", value: true }});
|
||||||
Router.events.on("beforeHistoryChange", handler);
|
Router.events.on("beforeHistoryChange", handler);
|
||||||
@@ -114,9 +118,26 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, transl
|
|||||||
placeholder="Text"
|
placeholder="Text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => isLoading || handleChange(e)}
|
onChange={e => isLoading || handleChange(e)}
|
||||||
|
onSubmit={useCallback(() => changeRoute(query), [query, changeRoute])}
|
||||||
lang={queryLang}
|
lang={queryLang}
|
||||||
audio={audio?.source}
|
audio={audio?.source}
|
||||||
/>
|
/>
|
||||||
|
<Stack direction={["row", null, "column"]} justify="center" spacing={3} px={[2, null, "initial"]}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Translate"
|
||||||
|
icon={<HiTranslate />}
|
||||||
|
colorScheme="lingva"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => changeRoute(query)}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
w={["full", null, "auto"]}
|
||||||
|
/>
|
||||||
|
<AutoTranslateButton
|
||||||
|
onAuto={useCallback(() => changeRoute(delayedQuery), [delayedQuery, changeRoute])}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
w={["full", null, "auto"]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<TranslationArea
|
<TranslationArea
|
||||||
id="translation"
|
id="translation"
|
||||||
aria-label="Translation result"
|
aria-label="Translation result"
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { render, screen, waitFor } from "@tests/reactUtils";
|
import { render, screen, waitFor } from "@tests/reactUtils";
|
||||||
import { htmlRes, resolveFetchWith } from "@tests/commonUtils";
|
import { htmlRes, resolveFetchWith } from "@tests/commonUtils";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import Router from "next/router";
|
|
||||||
import faker from "faker";
|
import faker from "faker";
|
||||||
import Page, { getStaticProps } from "@pages/[[...slug]]";
|
import Page, { getStaticProps } from "@pages/[[...slug]]";
|
||||||
|
import { localStorageSetMock } from "@mocks/localStorage";
|
||||||
const mockPush = jest.spyOn(Router, "push").mockImplementation(async () => true);
|
import { routerPushMock } from "@mocks/next";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.resetMocks();
|
fetchMock.resetMocks();
|
||||||
mockPush.mockReset();
|
routerPushMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getStaticProps", () => {
|
describe("getStaticProps", () => {
|
||||||
@@ -77,28 +76,125 @@ describe("Page", () => {
|
|||||||
expect(screen.getByText(/\xA9/)).toBeVisible();
|
expect(screen.getByText(/\xA9/)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches the page on query change", async () => {
|
it("switches the page on translate button click", async () => {
|
||||||
render(<Page home={true} />)
|
render(<Page home={true} />);
|
||||||
|
|
||||||
|
const query = screen.getByRole("textbox", { name: /translation query/i });
|
||||||
|
userEvent.type(query, faker.random.words());
|
||||||
|
const translate = screen.getByRole("button", { name: /translate/i });
|
||||||
|
translate.click();
|
||||||
|
|
||||||
|
expect(routerPushMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(screen.getByText(/loading translation/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't switch the page if nothing has changed", async () => {
|
||||||
|
const initial = {
|
||||||
|
source: "ca",
|
||||||
|
target: "es",
|
||||||
|
query: faker.random.words()
|
||||||
|
};
|
||||||
|
render(<Page translationRes={translationRes} audio={audio} initial={initial} />);
|
||||||
|
|
||||||
|
const translate = screen.getByRole("button", { name: /translate/i });
|
||||||
|
translate.click();
|
||||||
|
|
||||||
|
expect(routerPushMock).not.toHaveBeenCalled();
|
||||||
|
expect(screen.queryByText(/loading translation/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores auto state in localStorage", async () => {
|
||||||
|
render(<Page home={true} />);
|
||||||
|
|
||||||
|
const switchAuto = screen.getByRole("button", { name: /switch auto/i });
|
||||||
|
|
||||||
|
switchAuto.click();
|
||||||
|
await waitFor(() => expect(localStorageSetMock).toHaveBeenLastCalledWith("isauto", "true"));
|
||||||
|
switchAuto.click();
|
||||||
|
await waitFor(() => expect(localStorageSetMock).toHaveBeenLastCalledWith("isauto", "false"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches the page on query change if auto is enabled", async () => {
|
||||||
|
render(<Page home={true} />);
|
||||||
|
|
||||||
|
const switchAuto = screen.getByRole("button", { name: /switch auto/i });
|
||||||
|
switchAuto.click();
|
||||||
const query = screen.getByRole("textbox", { name: /translation query/i });
|
const query = screen.getByRole("textbox", { name: /translation query/i });
|
||||||
userEvent.type(query, faker.random.words());
|
userEvent.type(query, faker.random.words());
|
||||||
|
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
expect(Router.push).not.toHaveBeenCalled();
|
expect(routerPushMock).not.toHaveBeenCalled();
|
||||||
expect(screen.queryByText(/loading translation/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/loading translation/i)).not.toBeInTheDocument();
|
||||||
},
|
},
|
||||||
{ timeout: 250 }
|
{ timeout: 250 }
|
||||||
);
|
);
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
expect(Router.push).toHaveBeenCalledTimes(1);
|
expect(routerPushMock).toHaveBeenCalledTimes(1);
|
||||||
expect(screen.getByText(/loading translation/i)).toBeInTheDocument();
|
expect(screen.getByText(/loading translation/i)).toBeInTheDocument();
|
||||||
},
|
},
|
||||||
{ timeout: 2500 }
|
{ timeout: 2500 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches the page on language change if auto is enabled", async () => {
|
||||||
|
const initial = {
|
||||||
|
source: "auto",
|
||||||
|
target: "en",
|
||||||
|
query: faker.random.words()
|
||||||
|
};
|
||||||
|
render(<Page translationRes={translationRes} audio={audio} initial={initial} />);
|
||||||
|
|
||||||
|
const switchAuto = screen.getByRole("button", { name: /switch auto/i });
|
||||||
|
switchAuto.click();
|
||||||
|
|
||||||
|
const source = screen.getByRole("combobox", { name: /source language/i });
|
||||||
|
|
||||||
|
const sourceVal = "eo";
|
||||||
|
userEvent.selectOptions(source, sourceVal);
|
||||||
|
expect(source).toHaveValue(sourceVal);
|
||||||
|
|
||||||
|
await waitFor(() => expect(routerPushMock).toHaveBeenCalledTimes(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't switch the page on language change on the start page", async () => {
|
||||||
|
render(<Page home={true} />);
|
||||||
|
|
||||||
|
const switchAuto = screen.getByRole("button", { name: /switch auto/i });
|
||||||
|
switchAuto.click();
|
||||||
|
|
||||||
|
const source = screen.getByRole("combobox", { name: /source language/i });
|
||||||
|
|
||||||
|
const sourceVal = "eo";
|
||||||
|
userEvent.selectOptions(source, sourceVal);
|
||||||
|
expect(source).toHaveValue(sourceVal);
|
||||||
|
|
||||||
|
await waitFor(() => expect(routerPushMock).not.toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches languages & translations", async () => {
|
||||||
|
const initial = {
|
||||||
|
source: "es",
|
||||||
|
target: "ca",
|
||||||
|
query: faker.random.words()
|
||||||
|
};
|
||||||
|
render(<Page translationRes={translationRes} audio={audio} initial={initial} />);
|
||||||
|
|
||||||
|
const switchAuto = screen.getByRole("button", { name: /switch auto/i });
|
||||||
|
switchAuto.click();
|
||||||
|
|
||||||
|
const btnSwitch = screen.getByRole("button", { name: /switch languages/i });
|
||||||
|
userEvent.click(btnSwitch);
|
||||||
|
|
||||||
|
expect(screen.getByRole("combobox", { name: /source language/i })).toHaveValue(initial.target);
|
||||||
|
expect(screen.getByRole("combobox", { name: /target language/i })).toHaveValue(initial.source);
|
||||||
|
expect(screen.getByRole("textbox", { name: /translation query/i })).toHaveValue(translationRes);
|
||||||
|
expect(screen.getByRole("textbox", { name: /translation result/i })).toHaveValue(initial.query);
|
||||||
|
|
||||||
|
await waitFor(() => expect(routerPushMock).toHaveBeenCalledTimes(1));
|
||||||
|
});
|
||||||
|
|
||||||
it("translates & loads initials correctly", async () => {
|
it("translates & loads initials correctly", async () => {
|
||||||
const initial = {
|
const initial = {
|
||||||
source: "ca",
|
source: "ca",
|
||||||
@@ -117,54 +213,6 @@ describe("Page", () => {
|
|||||||
expect(translation).toHaveValue(translationRes);
|
expect(translation).toHaveValue(translationRes);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches the page on language change", async () => {
|
|
||||||
const initial = {
|
|
||||||
source: "auto",
|
|
||||||
target: "en",
|
|
||||||
query: faker.random.words()
|
|
||||||
};
|
|
||||||
render(<Page translationRes={translationRes} audio={audio} initial={initial} />);
|
|
||||||
|
|
||||||
const source = screen.getByRole("combobox", { name: /source language/i });
|
|
||||||
|
|
||||||
const sourceVal = "eo";
|
|
||||||
userEvent.selectOptions(source, sourceVal);
|
|
||||||
expect(source).toHaveValue(sourceVal);
|
|
||||||
|
|
||||||
await waitFor(() => expect(Router.push).toHaveBeenCalledTimes(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't switch the page on language change on the start page", async () => {
|
|
||||||
render(<Page home={true} />);
|
|
||||||
|
|
||||||
const source = screen.getByRole("combobox", { name: /source language/i });
|
|
||||||
|
|
||||||
const sourceVal = "eo";
|
|
||||||
userEvent.selectOptions(source, sourceVal);
|
|
||||||
expect(source).toHaveValue(sourceVal);
|
|
||||||
|
|
||||||
await waitFor(() => expect(Router.push).not.toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("switches languages & translations", async () => {
|
|
||||||
const initial = {
|
|
||||||
source: "es",
|
|
||||||
target: "ca",
|
|
||||||
query: faker.random.words()
|
|
||||||
};
|
|
||||||
render(<Page translationRes={translationRes} audio={audio} initial={initial} />);
|
|
||||||
|
|
||||||
const btnSwitch = screen.getByRole("button", { name: /switch languages/i });
|
|
||||||
userEvent.click(btnSwitch);
|
|
||||||
|
|
||||||
expect(screen.getByRole("combobox", { name: /source language/i })).toHaveValue(initial.target);
|
|
||||||
expect(screen.getByRole("combobox", { name: /target language/i })).toHaveValue(initial.source);
|
|
||||||
expect(screen.getByRole("textbox", { name: /translation query/i })).toHaveValue(translationRes);
|
|
||||||
expect(screen.getByRole("textbox", { name: /translation result/i })).toHaveValue("");
|
|
||||||
|
|
||||||
await waitFor(() => expect(Router.push).toHaveBeenCalledTimes(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("loads audio & clipboard correctly", async () => {
|
it("loads audio & clipboard correctly", async () => {
|
||||||
const initial = {
|
const initial = {
|
||||||
source: "eo",
|
source: "eo",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ it("switches the languages & the translations", () => {
|
|||||||
target: state.source,
|
target: state.source,
|
||||||
query: state.translation,
|
query: state.translation,
|
||||||
delayedQuery: state.translation,
|
delayedQuery: state.translation,
|
||||||
translation: "",
|
translation: state.query,
|
||||||
isLoading: initialState.isLoading
|
isLoading: initialState.isLoading
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function reducer(state: State, action: Action) {
|
|||||||
target,
|
target,
|
||||||
query: state.translation,
|
query: state.translation,
|
||||||
delayedQuery: state.translation,
|
delayedQuery: state.translation,
|
||||||
translation: ""
|
translation: state.query
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
Reference in New Issue
Block a user