Translation swap added

This commit is contained in:
David
2021-03-18 23:47:12 +01:00
parent 535f791455
commit 2bc931a4d3
7 changed files with 107 additions and 50 deletions

View File

@@ -8,6 +8,7 @@ beforeEach(() => {
it("switches page correctly on inputs change", () => { it("switches page correctly on inputs change", () => {
cy.findByRole("textbox", { name: /translation query/i }) cy.findByRole("textbox", { name: /translation query/i })
.as("query")
.type("palabra"); .type("palabra");
cy.findByRole("textbox", { name: /translation result/i }) cy.findByRole("textbox", { name: /translation result/i })
.as("translation") .as("translation")
@@ -15,27 +16,46 @@ it("switches page correctly on inputs change", () => {
.url() .url()
.should("include", "/auto/en/palabra"); .should("include", "/auto/en/palabra");
cy.findByRole("combobox", { name: /source language/i }) cy.findByRole("combobox", { name: /source language/i })
.as("source")
.select("es") .select("es")
.url() .url()
.should("include", "/es/en/palabra"); .should("include", "/es/en/palabra");
cy.findByRole("combobox", { name: /target language/i }) cy.findByRole("combobox", { name: /target language/i })
.select("ca"); .as("target")
cy.get("@translation") .select("ca")
.get("@translation")
.should("have.value", "paraula") .should("have.value", "paraula")
.url() .url()
.should("include", "/es/ca/palabra"); .should("include", "/es/ca/palabra");
cy.findByRole("button", { name: /switch languages/i })
.click()
.get("@source")
.should("have.value", "ca")
.get("@target")
.should("have.value", "es")
.get("@query")
.should("have.value", "paraula")
.get("@translation")
.should("have.value", "palabra")
.url()
.should("include", "/ca/es/paraula");
}); });
it("switches first loaded page on language change", () => { 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("textbox", { name: /translation query/i }) cy.findByRole("textbox", { name: /translation query/i })
.should("have.value", query); .should("have.value", query);
cy.findByRole("combobox", { name: /source language/i }) cy.findByRole("combobox", { name: /source language/i })
.as("source")
.select("eo") .select("eo")
.url() .url()
.should("include", `/eo/en/${encodeURIComponent(query)}`); .should("include", `/eo/en/${encodeURIComponent(query)}`)
.get("@source")
.select("auto")
.url()
.should("include", `/auto/en/${encodeURIComponent(query)}`);
}); });
it("doesn't switch initial page on language change", () => { it("doesn't switch initial page on language change", () => {
@@ -84,7 +104,7 @@ it("skips to main on 'skip link' click", () => {
.should("include", "#main"); .should("include", "#main");
}); });
it("shows error on >4 params", () => { it("shows error on >=4 params", () => {
cy.visit(`/auto/en/translation/other`) cy.visit("/auto/en/translation/other");
.findByText(404); cy.findByText(404);
}); });

View File

@@ -9,9 +9,8 @@ import { googleScrape, extractSlug } from "../utils/translate";
import { retrieveFiltered } from "../utils/language"; import { retrieveFiltered } from "../utils/language";
import langReducer, { Actions, initialState } from "../utils/reducer"; import langReducer, { Actions, initialState } from "../utils/reducer";
const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ translation, statusCode, errorMsg, initial }) => { const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ home, translationRes, statusCode, errorMsg, initial }) => {
const [{ source, target, query }, dispatch] = useReducer(langReducer, initialState); const [{ source, target, query, delayedQuery, translation }, dispatch] = useReducer(langReducer, initialState);
const [delayedQuery, setDelayedQuery] = useState(initialState.query);
const handleChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => { const handleChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLSelectElement>) => {
dispatch({ dispatch({
@@ -24,23 +23,27 @@ const Page: FC<InferGetStaticPropsType<typeof getStaticProps>> = ({ translation,
}; };
useEffect(() => { useEffect(() => {
initial && dispatch({ type: Actions.SET_ALL, payload: { state: initial }}); const state = { ...initial, delayedQuery: initial?.query, translation: translationRes };
}, [initial]); initial && dispatch({ type: Actions.SET_ALL, payload: { state }});
}, [initial, translationRes]);
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => setDelayedQuery(query), 1000); const timeout = setTimeout(() =>
dispatch({ type: Actions.SET_FIELD, payload: { key: "delayedQuery", value: query }}
), 1000);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [query]); }, [query]);
useEffect(() => { useEffect(() => {
const queryIsEmpty = !delayedQuery || delayedQuery === initialState.query; if (!delayedQuery || delayedQuery === initialState.query)
const queryIsInitial = delayedQuery === initial?.query; return;
const sourceIsInitial = source === initialState.source || source === initial?.source; if (!home && !initial)
const targetIsInitial = target === initialState.target || target === initial?.target; return;
const allAreInitials = queryIsInitial && sourceIsInitial && targetIsInitial; if (!home && delayedQuery === initial.query && source === initial.source && target === initial.target)
return;
queryIsEmpty || allAreInitials || Router.push(`/${source}/${target}/${encodeURIComponent(delayedQuery)}`); Router.push(`/${source}/${target}/${encodeURIComponent(delayedQuery)}`);
}, [source, target, delayedQuery, initial]); }, [source, target, delayedQuery, initial, home]);
const { sourceLangs, targetLangs } = retrieveFiltered(source, target); const { sourceLangs, targetLangs } = retrieveFiltered(source, target);
@@ -115,7 +118,7 @@ export const getStaticPaths: GetStaticPaths = async () => ({
export const getStaticProps: GetStaticProps = async ({ params }) => { export const getStaticProps: GetStaticProps = async ({ params }) => {
if (!params?.slug || !Array.isArray(params.slug)) if (!params?.slug || !Array.isArray(params.slug))
return { return {
props: {} props: { home: true }
}; };
const { source, target, query } = extractSlug(params.slug); const { source, target, query } = extractSlug(params.slug);

View File

@@ -17,11 +17,11 @@ describe("getStaticProps", () => {
const target = faker.random.locale(); const target = faker.random.locale();
const query = faker.random.words(); const query = faker.random.words();
it("returns empty props on empty params", async () => { it("returns home on empty params", async () => {
expect(await getStaticProps({ params: {} })).toStrictEqual({ props: {} }); expect(await getStaticProps({ params: {} })).toStrictEqual({ props: { home: true } });
}); });
it("returns not found on >4 params", async () => { it("returns not found on >=4 params", async () => {
const slug = [source, target, query, ""]; const slug = [source, target, query, ""];
expect(await getStaticProps({ params: { slug } })).toStrictEqual({ notFound: true }); expect(await getStaticProps({ params: { slug } })).toStrictEqual({ notFound: true });
}); });
@@ -37,14 +37,13 @@ describe("getStaticProps", () => {
}); });
it("returns translation & initial values on 3 params", async () => { it("returns translation & initial values on 3 params", async () => {
const translation = faker.random.words(); const translationRes = faker.random.words();
const html = htmlRes(translation); resolveFetchWith(htmlRes(translationRes));
resolveFetchWith(html);
const slug = [source, target, query]; const slug = [source, target, query];
expect(await getStaticProps({ params: { slug } })).toStrictEqual({ expect(await getStaticProps({ params: { slug } })).toStrictEqual({
props: { props: {
translation, translationRes,
initial: { initial: {
source, source,
target, target,
@@ -58,7 +57,7 @@ describe("getStaticProps", () => {
describe("Page", () => { describe("Page", () => {
it("loads the layout correctly", async () => { it("loads the layout correctly", async () => {
render(<Page />); render(<Page home={true} />);
expect(screen.getByRole("link", { name: /skip to content/i })).toBeEnabled(); expect(screen.getByRole("link", { name: /skip to content/i })).toBeEnabled();
expect(await screen.findByRole("img", { name: /logo/i })).toBeVisible(); expect(await screen.findByRole("img", { name: /logo/i })).toBeVisible();
@@ -68,7 +67,7 @@ describe("Page", () => {
}); });
it("switches the page on query change", async () => { it("switches the page on query change", async () => {
render(<Page />) render(<Page home={true} />)
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());
@@ -89,8 +88,8 @@ describe("Page", () => {
target: "es", target: "es",
query: faker.random.words() query: faker.random.words()
}; };
const translationText = faker.random.words(); const translationRes = faker.random.words();
render(<Page translation={translationText} initial={initial} />); render(<Page translationRes={translationRes} initial={initial} />);
const source = screen.getByRole("combobox", { name: /source language/i }); const source = screen.getByRole("combobox", { name: /source language/i });
expect(source).toHaveValue(initial.source); expect(source).toHaveValue(initial.source);
@@ -99,7 +98,7 @@ describe("Page", () => {
const query = screen.getByRole("textbox", { name: /translation query/i }); const query = screen.getByRole("textbox", { name: /translation query/i });
expect(query).toHaveValue(initial.query); expect(query).toHaveValue(initial.query);
const translation = screen.getByRole("textbox", { name: /translation result/i }); const translation = screen.getByRole("textbox", { name: /translation result/i });
expect(translation).toHaveValue(translationText); expect(translation).toHaveValue(translationRes);
}); });
it("switches the page on language change", async () => { it("switches the page on language change", async () => {
@@ -108,8 +107,8 @@ describe("Page", () => {
target: "en", target: "en",
query: faker.random.words() query: faker.random.words()
}; };
const translationText = faker.random.words(); const translationRes = faker.random.words();
render(<Page translation={translationText} initial={initial} />); render(<Page translationRes={translationRes} initial={initial} />);
const source = screen.getByRole("combobox", { name: /source language/i }); const source = screen.getByRole("combobox", { name: /source language/i });
@@ -121,7 +120,7 @@ describe("Page", () => {
}); });
it("doesn't switch the page on language change on the start page", async () => { it("doesn't switch the page on language change on the start page", async () => {
render(<Page />); render(<Page home={true} />);
const source = screen.getByRole("combobox", { name: /source language/i }); const source = screen.getByRole("combobox", { name: /source language/i });
@@ -132,6 +131,26 @@ describe("Page", () => {
await waitFor(() => expect(Router.push).not.toHaveBeenCalled()); await waitFor(() => expect(Router.push).not.toHaveBeenCalled());
}); });
it("switches languages & translations", async () => {
const initial = {
source: "es",
target: "ca",
query: faker.random.words()
};
const translationRes = faker.random.words();
render(<Page translationRes={translationRes} 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("renders error page on status code", async () => { it("renders error page on status code", async () => {
const code = faker.random.number({ min: 400, max: 599 }); const code = faker.random.number({ min: 400, max: 599 });
render(<Page statusCode={code} />); render(<Page statusCode={code} />);

View File

@@ -15,10 +15,13 @@ it("changes a field value", () => {
}); });
it("changes all fields", () => { it("changes all fields", () => {
const query = faker.random.words();
const state = { const state = {
source: faker.random.locale(), source: faker.random.locale(),
target: faker.random.locale(), target: faker.random.locale(),
query: faker.random.words() query,
delayedQuery: query,
translation: faker.random.words()
}; };
const res = langReducer(initialState, { const res = langReducer(initialState, {
@@ -28,16 +31,23 @@ it("changes all fields", () => {
expect(res).toStrictEqual(state); expect(res).toStrictEqual(state);
}); });
it("switches the languages", () => { it("switches the languages & the translations", () => {
const state = { const state = {
...initialState, ...initialState,
source: "es", source: "es",
target: "ca" target: "ca",
query: faker.random.words(),
translation: faker.random.words()
}; };
const res = langReducer(state, { type: Actions.SWITCH_LANGS }); const res = langReducer(state, { type: Actions.SWITCH_LANGS });
expect(res.source).toStrictEqual(state.target); expect(res).toStrictEqual({
expect(res.target).toStrictEqual(state.source); source: state.target,
target: state.source,
query: state.translation,
delayedQuery: state.translation,
translation: ""
});
}); });
it("resets the source while switching if they're the same", () => { it("resets the source while switching if they're the same", () => {

View File

@@ -12,11 +12,11 @@ describe("googleScrape", () => {
}); });
it("parses html response correctly", async () => { it("parses html response correctly", async () => {
const translation = faker.random.words(); const translationRes = faker.random.words();
const html = htmlRes(translation); const html = htmlRes(translationRes);
resolveFetchWith(html); resolveFetchWith(html);
expect(await googleScrape(source, target, query)).toStrictEqual({ translation }); expect(await googleScrape(source, target, query)).toStrictEqual({ translationRes });
}); });
it("returns status code on request error", async () => { it("returns status code on request error", async () => {

View File

@@ -3,7 +3,9 @@ import { replaceBoth } from "./language";
export const initialState = { export const initialState = {
source: "auto", source: "auto",
target: "en", target: "en",
query: "" query: "",
delayedQuery: "",
translation: ""
} }
type State = typeof initialState; type State = typeof initialState;
@@ -46,7 +48,10 @@ export default function reducer(state: State, action: Action) {
source: source !== target source: source !== target
? source ? source
: initialState.source, : initialState.source,
target target,
query: state.translation,
delayedQuery: state.translation,
translation: ""
}; };
default: default:
return state; return state;

View File

@@ -6,7 +6,7 @@ export async function googleScrape(
target: string, target: string,
query: string query: string
): Promise<{ ): Promise<{
translation?: string, translationRes?: string,
statusCode?: number, statusCode?: number,
errorMsg?: string errorMsg?: string
}> { }> {
@@ -26,11 +26,11 @@ export async function googleScrape(
}; };
const html = await res.text(); const html = await res.text();
const translation = cheerio.load(html)(".result-container").text().trim(); const translationRes = cheerio.load(html)(".result-container").text().trim();
return translation return translationRes
? { ? {
translation translationRes
} : { } : {
errorMsg: "An error occurred while parsing the translation" errorMsg: "An error occurred while parsing the translation"
}; };