diff --git a/cypress/integration/app.spec.ts b/cypress/integration/app.spec.ts index a5881bf..5177158 100644 --- a/cypress/integration/app.spec.ts +++ b/cypress/integration/app.spec.ts @@ -8,6 +8,7 @@ beforeEach(() => { it("switches page correctly on inputs change", () => { cy.findByRole("textbox", { name: /translation query/i }) + .as("query") .type("palabra"); cy.findByRole("textbox", { name: /translation result/i }) .as("translation") @@ -15,27 +16,46 @@ it("switches page correctly on inputs change", () => { .url() .should("include", "/auto/en/palabra"); cy.findByRole("combobox", { name: /source language/i }) + .as("source") .select("es") .url() .should("include", "/es/en/palabra"); cy.findByRole("combobox", { name: /target language/i }) - .select("ca"); - cy.get("@translation") + .as("target") + .select("ca") + .get("@translation") .should("have.value", "paraula") .url() .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(); cy.visit(`/auto/en/${query}`); cy.findByRole("textbox", { name: /translation query/i }) .should("have.value", query); cy.findByRole("combobox", { name: /source language/i }) + .as("source") .select("eo") .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", () => { @@ -84,7 +104,7 @@ it("skips to main on 'skip link' click", () => { .should("include", "#main"); }); -it("shows error on >4 params", () => { - cy.visit(`/auto/en/translation/other`) - .findByText(404); +it("shows error on >=4 params", () => { + cy.visit("/auto/en/translation/other"); + cy.findByText(404); }); diff --git a/pages/[[...slug]].tsx b/pages/[[...slug]].tsx index 424322f..a2865dd 100644 --- a/pages/[[...slug]].tsx +++ b/pages/[[...slug]].tsx @@ -9,9 +9,8 @@ import { googleScrape, extractSlug } from "../utils/translate"; import { retrieveFiltered } from "../utils/language"; import langReducer, { Actions, initialState } from "../utils/reducer"; -const Page: FC> = ({ translation, statusCode, errorMsg, initial }) => { - const [{ source, target, query }, dispatch] = useReducer(langReducer, initialState); - const [delayedQuery, setDelayedQuery] = useState(initialState.query); +const Page: FC> = ({ home, translationRes, statusCode, errorMsg, initial }) => { + const [{ source, target, query, delayedQuery, translation }, dispatch] = useReducer(langReducer, initialState); const handleChange = (e: ChangeEvent) => { dispatch({ @@ -24,23 +23,27 @@ const Page: FC> = ({ translation, }; useEffect(() => { - initial && dispatch({ type: Actions.SET_ALL, payload: { state: initial }}); - }, [initial]); + const state = { ...initial, delayedQuery: initial?.query, translation: translationRes }; + initial && dispatch({ type: Actions.SET_ALL, payload: { state }}); + }, [initial, translationRes]); useEffect(() => { - const timeout = setTimeout(() => setDelayedQuery(query), 1000); + const timeout = setTimeout(() => + dispatch({ type: Actions.SET_FIELD, payload: { key: "delayedQuery", value: query }} + ), 1000); return () => clearTimeout(timeout); }, [query]); useEffect(() => { - const queryIsEmpty = !delayedQuery || delayedQuery === initialState.query; - const queryIsInitial = delayedQuery === initial?.query; - const sourceIsInitial = source === initialState.source || source === initial?.source; - const targetIsInitial = target === initialState.target || target === initial?.target; - const allAreInitials = queryIsInitial && sourceIsInitial && targetIsInitial; + if (!delayedQuery || delayedQuery === initialState.query) + return; + if (!home && !initial) + return; + if (!home && delayedQuery === initial.query && source === initial.source && target === initial.target) + return; - queryIsEmpty || allAreInitials || Router.push(`/${source}/${target}/${encodeURIComponent(delayedQuery)}`); - }, [source, target, delayedQuery, initial]); + Router.push(`/${source}/${target}/${encodeURIComponent(delayedQuery)}`); + }, [source, target, delayedQuery, initial, home]); const { sourceLangs, targetLangs } = retrieveFiltered(source, target); @@ -115,7 +118,7 @@ export const getStaticPaths: GetStaticPaths = async () => ({ export const getStaticProps: GetStaticProps = async ({ params }) => { if (!params?.slug || !Array.isArray(params.slug)) return { - props: {} + props: { home: true } }; const { source, target, query } = extractSlug(params.slug); diff --git a/tests/pages/[[...slug]].test.tsx b/tests/pages/[[...slug]].test.tsx index 6a198a6..c61860a 100644 --- a/tests/pages/[[...slug]].test.tsx +++ b/tests/pages/[[...slug]].test.tsx @@ -17,11 +17,11 @@ describe("getStaticProps", () => { const target = faker.random.locale(); const query = faker.random.words(); - it("returns empty props on empty params", async () => { - expect(await getStaticProps({ params: {} })).toStrictEqual({ props: {} }); + it("returns home on empty params", async () => { + 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, ""]; expect(await getStaticProps({ params: { slug } })).toStrictEqual({ notFound: true }); }); @@ -37,14 +37,13 @@ describe("getStaticProps", () => { }); it("returns translation & initial values on 3 params", async () => { - const translation = faker.random.words(); - const html = htmlRes(translation); - resolveFetchWith(html); + const translationRes = faker.random.words(); + resolveFetchWith(htmlRes(translationRes)); const slug = [source, target, query]; expect(await getStaticProps({ params: { slug } })).toStrictEqual({ props: { - translation, + translationRes, initial: { source, target, @@ -58,7 +57,7 @@ describe("getStaticProps", () => { describe("Page", () => { it("loads the layout correctly", async () => { - render(); + render(); expect(screen.getByRole("link", { name: /skip to content/i })).toBeEnabled(); expect(await screen.findByRole("img", { name: /logo/i })).toBeVisible(); @@ -68,7 +67,7 @@ describe("Page", () => { }); it("switches the page on query change", async () => { - render() + render() const query = screen.getByRole("textbox", { name: /translation query/i }); userEvent.type(query, faker.random.words()); @@ -89,8 +88,8 @@ describe("Page", () => { target: "es", query: faker.random.words() }; - const translationText = faker.random.words(); - render(); + const translationRes = faker.random.words(); + render(); const source = screen.getByRole("combobox", { name: /source language/i }); expect(source).toHaveValue(initial.source); @@ -99,7 +98,7 @@ describe("Page", () => { const query = screen.getByRole("textbox", { name: /translation query/i }); expect(query).toHaveValue(initial.query); 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 () => { @@ -108,8 +107,8 @@ describe("Page", () => { target: "en", query: faker.random.words() }; - const translationText = faker.random.words(); - render(); + const translationRes = faker.random.words(); + render(); 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 () => { - render(); + render(); const source = screen.getByRole("combobox", { name: /source language/i }); @@ -132,6 +131,26 @@ describe("Page", () => { 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(); + + 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 () => { const code = faker.random.number({ min: 400, max: 599 }); render(); diff --git a/tests/utils/reducer.test.ts b/tests/utils/reducer.test.ts index 13431ed..b5a5a59 100644 --- a/tests/utils/reducer.test.ts +++ b/tests/utils/reducer.test.ts @@ -15,10 +15,13 @@ it("changes a field value", () => { }); it("changes all fields", () => { + const query = faker.random.words(); const state = { source: faker.random.locale(), target: faker.random.locale(), - query: faker.random.words() + query, + delayedQuery: query, + translation: faker.random.words() }; const res = langReducer(initialState, { @@ -28,16 +31,23 @@ it("changes all fields", () => { expect(res).toStrictEqual(state); }); -it("switches the languages", () => { +it("switches the languages & the translations", () => { const state = { ...initialState, source: "es", - target: "ca" + target: "ca", + query: faker.random.words(), + translation: faker.random.words() }; const res = langReducer(state, { type: Actions.SWITCH_LANGS }); - expect(res.source).toStrictEqual(state.target); - expect(res.target).toStrictEqual(state.source); + expect(res).toStrictEqual({ + source: state.target, + target: state.source, + query: state.translation, + delayedQuery: state.translation, + translation: "" + }); }); it("resets the source while switching if they're the same", () => { diff --git a/tests/utils/translate.test.ts b/tests/utils/translate.test.ts index e52c6f9..8e6dd2f 100644 --- a/tests/utils/translate.test.ts +++ b/tests/utils/translate.test.ts @@ -12,11 +12,11 @@ describe("googleScrape", () => { }); it("parses html response correctly", async () => { - const translation = faker.random.words(); - const html = htmlRes(translation); + const translationRes = faker.random.words(); + const html = htmlRes(translationRes); 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 () => { diff --git a/utils/reducer.ts b/utils/reducer.ts index 5d75a7b..e472b9a 100644 --- a/utils/reducer.ts +++ b/utils/reducer.ts @@ -3,7 +3,9 @@ import { replaceBoth } from "./language"; export const initialState = { source: "auto", target: "en", - query: "" + query: "", + delayedQuery: "", + translation: "" } type State = typeof initialState; @@ -46,7 +48,10 @@ export default function reducer(state: State, action: Action) { source: source !== target ? source : initialState.source, - target + target, + query: state.translation, + delayedQuery: state.translation, + translation: "" }; default: return state; diff --git a/utils/translate.ts b/utils/translate.ts index e1a83d6..b99962f 100644 --- a/utils/translate.ts +++ b/utils/translate.ts @@ -6,7 +6,7 @@ export async function googleScrape( target: string, query: string ): Promise<{ - translation?: string, + translationRes?: string, statusCode?: number, errorMsg?: string }> { @@ -26,11 +26,11 @@ export async function googleScrape( }; 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" };