diff --git a/jest.config.js b/jest.config.js index 7292d1b..be5b69d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,11 +7,11 @@ module.exports = { '^.+\\.module\\.(css|sass|scss)$', ], testMatch: [ - '/**/__tests__/**/*.{js,jsx,ts,tsx}', - '/**/*.{spec,test}.{js,jsx,ts,tsx}', + '/**/tests/*/**/*.{js,jsx,ts,tsx}', + '/**/*.{spec,test}.{js,jsx,ts,tsx}' ], setupFilesAfterEnv: [ - "/setupTests.ts" + "/tests/setupTests.ts" ], moduleFileExtensions: ["ts", "tsx", "js", "jsx"] } diff --git a/package.json b/package.json index 9cd3f63..c0977c5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.3", + "@types/faker": "^5.1.7", "@types/jest": "^26.0.20", "@types/node": "^14.14.33", "@types/react": "^17.0.3", @@ -34,7 +35,9 @@ "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.8", "eslint-plugin-testing-library": "^3.9.0", + "faker": "^5.4.0", "jest": "^26.6.3", + "jest-fetch-mock": "^3.0.3", "typescript": "^4.2.3" }, "eslintConfig": { @@ -44,6 +47,8 @@ ] }, "babel": { - "presets": ["next/babel"] + "presets": [ + "next/babel" + ] } } diff --git a/pages/[[...slug]].tsx b/pages/[[...slug]].tsx index e8b03ff..08c32c8 100644 --- a/pages/[[...slug]].tsx +++ b/pages/[[...slug]].tsx @@ -8,7 +8,7 @@ import Languages from "../components/Languages"; import { retrieveFiltered } from "../utils/language"; import langReducer, { Actions, initialState } from "../utils/reducer"; -const Page: FC> = ({ translation, error, initial }) => { +const Page: FC> = ({ translation, statusCode, errorMsg, initial }) => { const [{ source, target, query }, dispatch] = useReducer(langReducer, initialState); const [encodedQuery, setEncodedQuery] = useState(""); @@ -40,8 +40,8 @@ const Page: FC> = ({ translation, const { sourceLangs, targetLangs } = retrieveFiltered(source, target); - return error ? ( - + return statusCode ? ( + ) : (
diff --git a/setupTests.ts b/setupTests.ts deleted file mode 100644 index 7b0828b..0000000 --- a/setupTests.ts +++ /dev/null @@ -1 +0,0 @@ -import '@testing-library/jest-dom'; diff --git a/__tests__/Error.test.tsx b/tests/pages/Error.test.tsx similarity index 82% rename from __tests__/Error.test.tsx rename to tests/pages/Error.test.tsx index fcf7551..75f67db 100644 --- a/__tests__/Error.test.tsx +++ b/tests/pages/Error.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import faker from "faker"; import Error from "next/error"; it("renders a not found message on 404 code", () => { @@ -7,7 +8,7 @@ it("renders a not found message on 404 code", () => { }); it("renders the correct status code", () => { - const code = Math.floor(Math.random() * 500) + 100; + const code = faker.random.number({ min: 100, max: 599 }); render(); expect(screen.getByText(code)).toBeVisible(); }); diff --git a/tests/setupTests.ts b/tests/setupTests.ts new file mode 100644 index 0000000..d2d4c89 --- /dev/null +++ b/tests/setupTests.ts @@ -0,0 +1,3 @@ +import '@testing-library/jest-dom'; +import jestFetchMock from 'jest-fetch-mock'; +jestFetchMock.enableMocks(); diff --git a/tests/utils/translate.test.ts b/tests/utils/translate.test.ts new file mode 100644 index 0000000..70a8c0a --- /dev/null +++ b/tests/utils/translate.test.ts @@ -0,0 +1,75 @@ +import faker from "faker"; +import { googleScrape, extractSlug } from "../../utils/translate"; + +const source = faker.random.locale(); +const target = faker.random.locale(); +const query = faker.random.words(); + +describe("googleScrape", () => { + beforeEach(() => { + fetchMock.resetMocks(); + }); + + it("parses html response correctly", async () => { + const translation = faker.random.words(); + const className = "result-container"; + const html = ` +
+ ${translation} +
+ `; + fetchMock.mockResponseOnce(async () => ({ body: html })); + + expect(await googleScrape(source, target, query)).toStrictEqual({ translation }); + }); + + it("returns status code on request error", async () => { + const status = faker.random.number({ min: 400, max: 499 }); + fetchMock.mockResponseOnce(async () => ({ status })); + + expect(await googleScrape(source, target, query)).toStrictEqual({ statusCode: status }); + }); + + it("returns correct message on network error", async () => { + fetchMock.mockRejectOnce(); + + const res = await googleScrape(source, target, query); + expect(res?.errorMsg).toMatch(/retrieving/); + }); + + it("returns correct message on parsing wrong class", async () => { + const translation = faker.random.words(); + const className = "wrong-container"; + const html = ` +
+ ${translation} +
+ `; + fetchMock.mockResponseOnce(async () => ({ body: html })); + + const res = await googleScrape(source, target, query); + expect(res?.errorMsg).toMatch(/parsing/); + }); +}); + +describe("extractSlug", () => { + it("returns 'query' for 1 param", () => { + expect(extractSlug([query])).toStrictEqual({ query }); + }); + + it("returns 'target' & 'query' resp. for 2 params", () => { + expect(extractSlug([target, query])).toStrictEqual({ target, query }); + }); + + it("returns 'source', 'target' & 'query' resp. for 3 param", () => { + expect(extractSlug([source, target, query])).toStrictEqual({ source, target, query }); + }); + + it("returns empty object on 0 or >4 params", () => { + expect(extractSlug([])).toStrictEqual({}); + + const length = faker.random.number({ min: 4, max: 50 }); + const array = Array(length).fill(""); + expect(extractSlug(array)).toStrictEqual({}); + }); +}); diff --git a/utils/translate.ts b/utils/translate.ts index ecfdde1..e1a83d6 100644 --- a/utils/translate.ts +++ b/utils/translate.ts @@ -7,20 +7,33 @@ export async function googleScrape( query: string ): Promise<{ translation?: string, - error?: number + statusCode?: number, + errorMsg?: string }> { const parsed = replaceBoth("mapping", { source, target }); - const res = await fetch(`https://translate.google.com/m?sl=${parsed.source}&tl=${parsed.target}&q=${encodeURIComponent(query)}`); + const res = await fetch( + `https://translate.google.com/m?sl=${parsed.source}&tl=${parsed.target}&q=${encodeURIComponent(query)}` + ).catch(() => null); + + if (!res) + return { + errorMsg: "An error occurred while retrieving the translation" + } if (!res.ok) return { - error: res.status + statusCode: res.status }; const html = await res.text(); - return { - translation: cheerio.load(html)(".result-container").text() - }; + const translation = cheerio.load(html)(".result-container").text().trim(); + + return translation + ? { + translation + } : { + errorMsg: "An error occurred while parsing the translation" + }; } export function extractSlug(slug: string[]): { @@ -33,10 +46,10 @@ export function extractSlug(slug: string[]): { case 1: return { query: p1 }; case 2: - return { target: p1, query: p2 } + return { target: p1, query: p2 }; case 3: - return { source: p1, target: p2, query: p3 } + return { source: p1, target: p2, query: p3 }; default: - return {} + return {}; } } diff --git a/yarn.lock b/yarn.lock index ee80767..fe2052d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -719,6 +719,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/faker@^5.1.7": + version "5.1.7" + resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.1.7.tgz#1c3f0655f6e912f5578ce40baa941ab8705fc0ef" + integrity sha512-ByseCEyhb+64fapaFR/yASUfAU+Ia4LpCnHhoNMQJFSIBvbtFoEZQB0elwrSF/idpKL5OZvmZhCCukGV8Zi22w== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1800,6 +1805,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" + integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -2586,6 +2598,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +faker@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/faker/-/faker-5.4.0.tgz#f18e55993c6887918182b003d163df14daeb3011" + integrity sha512-Y9n/Ky/xZx/Bj8DePvXspUYRtHl/rGQytoIT5LaxmNwSe3wWyOeOXb3lT6Dpipq240PVpeFaGKzScz/5fvff2g== + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3480,6 +3497,14 @@ jest-environment-node@^26.6.2: jest-mock "^26.6.2" jest-util "^26.6.2" +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -4759,6 +4784,11 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-polyfill@^8.1.3: + version "8.2.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" + integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g== + prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7"