diff --git a/controllers/generateConfigController.test.js b/controllers/generateConfigController.test.js index 2a6e891..8e8580e 100644 --- a/controllers/generateConfigController.test.js +++ b/controllers/generateConfigController.test.js @@ -11,13 +11,6 @@ jest.unstable_mockModule("./utils/listFiles.js", () => ({ listFilesOnDirectory: jest.fn(async () => Promise.resolve([])), })); -jest.unstable_mockModule("fs", () => ({ - default: { - existsSync: jest.fn(() => false), - writeFileSync: jest.fn(), - }, -})); - const { confirmUserAction, promptUserInput } = await import( "../utils/promptUtils.js" ); diff --git a/controllers/translateController.test.js b/controllers/translateController.test.js index 94e0bbb..001516c 100644 --- a/controllers/translateController.test.js +++ b/controllers/translateController.test.js @@ -1,26 +1,40 @@ import { translate } from "@vitalets/google-translate-api"; -import translateController from "./translateController.js"; import { configPath, parsePath } from "../utils/getConfigPath.js"; -import { getOrCreateJsonFile } from "../utils/getOrCreateJsonFile.js"; -import promptFactory from "prompt-sync-plus"; -const prompt = promptFactory(); -import fs from "fs"; +import prompt from "../utils/promptUser.js"; import { validEngines } from "../services/translateService.js"; -import { - getLanguagesCodesWithNames, - supportedLanguages, -} from "../utils/supportedLanguagesUtils.js"; -import { importJSONFile } from "../utils/importJSONFile.js"; - -const config = await importJSONFile(parsePath(configPath)); - -/** - * TODO: Update tests to use a mocked version of fs instead of creating files on the disk and reading them. - * So tests could work fine on jest using modules (for the error: - * ReferenceError: You are trying to `import` a file after the Jest environment has been torn down. From controllers/translateController.test.js.) - * And we can avoid random failing tests on CI/CD pipelines. - */ -describe.skip("TranslateController", () => { +import fs from "fs"; + +import { jest } from "@jest/globals"; + +const config = { + languages: [ + { + name: "en", + files: ["en.json"], + }, + { + name: "es", + files: ["es.json"], + }, + ], + basePath: "test", + translationEngines: ["google", "bing", "libreTranslate"], +}; + +jest.unstable_mockModule("./utils/importJSONFile.js", () => ({ + importJSONFile: jest.fn(async () => config), +})); + +const { importJSONFile } = await import("../utils/importJSONFile.js"); +const { getOrCreateJsonFile } = await import("../utils/getOrCreateJsonFile.js"); +const { getLanguagesCodesWithNames, supportedLanguages } = await import( + "../utils/supportedLanguagesUtils.js" +); +const translateController = ( + await import("../controllers/translateController.js") +).default; + +describe("TranslateController", () => { let text, sourceLanguage, nameOfTranslation; beforeEach(() => { jest.clearAllMocks(); @@ -29,15 +43,7 @@ describe.skip("TranslateController", () => { sourceLanguage = "en"; nameOfTranslation = "helloWorld"; - if (fs.existsSync("test-configs")) { - fs.rmSync("test-configs", { recursive: true }); - } - }); - - afterEach(() => { - if (fs.existsSync("test-configs")) { - fs.rmSync("test-configs", { recursive: true }); - } + fs.existsSync.mockImplementation(() => true); }); it("It's calling translate function for each language on config path except for the source language", async () => { @@ -58,6 +64,8 @@ describe.skip("TranslateController", () => { }); it("It's throwing an error if no config file is found", async () => { + fs.existsSync.mockImplementationOnce(() => false); + await expect( translateController(text, sourceLanguage, nameOfTranslation, { settingsFile: "non-existent-file", @@ -65,54 +73,20 @@ describe.skip("TranslateController", () => { ).rejects.toThrow("No settings file found"); }); - it("It's handling properly custom config files", async () => { - const customConfig = { - basePath: "test-configs/translations", - languages: [ - { - name: "es", - files: ["es.json"], - }, - { - name: "fr", - files: ["fr.json"], - }, - ], - }; - - await getOrCreateJsonFile( - "test-configs", - "custom-config.json", - customConfig, - ); - - fs.writeFileSync( - "test-configs/custom-config.json", - JSON.stringify(customConfig, null, 2), - ); + it("It's calling translate function for each language on config path", async () => { + const { languages } = config; - await translateController(text, sourceLanguage, nameOfTranslation, { - settingsFile: "test-configs/custom-config.json", + await translateController(text, "fr", nameOfTranslation, { + settingsFile: configPath, }); - expect(translate).toHaveBeenCalledTimes(2); + expect(translate).toHaveBeenCalledTimes(languages.length); }); it("It's throwing an error if no languages are found on config file", async () => { - const noLanguagesConfig = { + importJSONFile.mockImplementationOnce(async () => ({ basePath: "test-configs/translations", - }; - - await getOrCreateJsonFile( - "test-configs", - "no-languages.json", - noLanguagesConfig, - ); - - fs.writeFileSync( - "test-configs/no-languages.json", - JSON.stringify(noLanguagesConfig, null, 2), - ); + })); await expect( translateController(text, sourceLanguage, nameOfTranslation, { @@ -135,15 +109,8 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile( - "test-configs", - "invalid-translation-engine.json", - invalidTranslationEngineConfig, - ); - - fs.writeFileSync( - "test-configs/invalid-translation-engine.json", - JSON.stringify(invalidTranslationEngineConfig, null, 2), + importJSONFile.mockImplementationOnce( + async () => invalidTranslationEngineConfig, ); await expect( @@ -411,16 +378,7 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile( - "test-configs", - "test-config-without-name.json", - testConfig, - ); - - fs.writeFileSync( - "test-configs/test-config-without-name.json", - JSON.stringify(testConfig, null, 2), - ); + importJSONFile.mockImplementationOnce(async () => testConfig); await expect( translateController(text, sourceLanguage, nameOfTranslation, { @@ -439,16 +397,7 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile( - "test-configs", - "test-config-without-files.json", - testConfig, - ); - - fs.writeFileSync( - "test-configs/test-config-without-files.json", - JSON.stringify(testConfig, null, 2), - ); + importJSONFile.mockImplementationOnce(async () => testConfig); await expect( translateController(text, sourceLanguage, nameOfTranslation, { @@ -459,7 +408,7 @@ describe.skip("TranslateController", () => { ); }); - it("It's throwing an error if files empties are provided on files array", async () => { + it("It's throwing an error if empty files are provided on files array", async () => { const testConfig = { basePath: "test-configs/translations", languages: [ @@ -470,16 +419,7 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile( - "test-configs", - "test-config-with-empty-files.json", - testConfig, - ); - - fs.writeFileSync( - "test-configs/test-config-with-empty-files.json", - JSON.stringify(testConfig, null, 2), - ); + importJSONFile.mockImplementationOnce(async () => testConfig); await expect( translateController(text, sourceLanguage, nameOfTranslation, { @@ -501,24 +441,15 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile("test-configs", "test-config.json", testConfig); - - fs.writeFileSync( - "test-configs/test-config.json", - JSON.stringify(testConfig, null, 2), - ); - - const { file } = await getOrCreateJsonFile( - testConfig.basePath, - testConfig.languages[0].files[0], - ); + const file = { + [nameOfTranslation]: text, + }; - file[nameOfTranslation] = text; + importJSONFile.mockImplementation(async (path) => { + if (path.includes("test-configs/test-config.json")) return testConfig; - fs.writeFileSync( - `${testConfig.basePath}/${testConfig.languages[0].files[0]}`, - JSON.stringify(file, null, 2), - ); + return file; + }); await translateController(text, sourceLanguage, nameOfTranslation, { settingsFile: "test-configs/test-config.json", @@ -538,24 +469,15 @@ describe.skip("TranslateController", () => { ], }; - await getOrCreateJsonFile("test-configs", "test-config.json", testConfig); - - fs.writeFileSync( - "test-configs/test-config.json", - JSON.stringify(testConfig, null, 2), - ); - - const { file } = await getOrCreateJsonFile( - testConfig.basePath, - testConfig.languages[0].files[0], - ); + const file = { + [nameOfTranslation]: text, + }; - file[nameOfTranslation] = text; + importJSONFile.mockImplementation(async (path) => { + if (path.includes("test-configs/test-config.json")) return testConfig; - fs.writeFileSync( - `${testConfig.basePath}/${testConfig.languages[0].files[0]}`, - JSON.stringify(file, null, 2), - ); + return file; + }); prompt.mockImplementationOnce(() => "no"); @@ -563,27 +485,34 @@ describe.skip("TranslateController", () => { settingsFile: "test-configs/test-config.json", }); - const { file: esFile } = await getOrCreateJsonFile( - testConfig.basePath, - testConfig.languages[0].files[0], - ); - const { file: esMxFile } = await getOrCreateJsonFile( - testConfig.basePath, - testConfig.languages[0].files[1], - ); - const { file: esEsFile } = await getOrCreateJsonFile( - testConfig.basePath, - testConfig.languages[0].files[2], + expect(fs.writeFileSync).toHaveBeenNthCalledWith( + 1, + parsePath(`${testConfig.basePath}/${testConfig.languages[0].files[1]}`), + JSON.stringify( + { + [nameOfTranslation]: `${text} translated from ${sourceLanguage} to ${testConfig.languages[0].name}`, + }, + null, + 2, + ), ); - expect(esFile[nameOfTranslation]).toBe(text); - expect(esMxFile[nameOfTranslation]).toBe( - `${text} translated from ${sourceLanguage} to ${testConfig.languages[0].name}`, - ); - expect(esEsFile[nameOfTranslation]).toBe( - `${text} translated from ${sourceLanguage} to ${testConfig.languages[0].name}`, + expect(fs.writeFileSync).toHaveBeenNthCalledWith( + 2, + parsePath(`${testConfig.basePath}/${testConfig.languages[0].files[2]}`), + JSON.stringify( + { + [nameOfTranslation]: `${text} translated from ${sourceLanguage} to ${testConfig.languages[0].name}`, + }, + null, + 2, + ), ); - expect(prompt).toHaveBeenCalledTimes(1); + expect(fs.writeFileSync).toHaveBeenCalledTimes(2); + + expect(fs.writeFileSync).toHaveBeenCalledTimes(2); + + expect(prompt).toHaveBeenCalledTimes(3); }); }); diff --git a/jest.setup.mjs b/jest.setup.mjs index 8563dcd..e87cc2b 100644 --- a/jest.setup.mjs +++ b/jest.setup.mjs @@ -10,6 +10,15 @@ jest.unstable_mockModule("./utils/promptUser.js", () => { return { default: jest.fn(() => "yes") }; }); +jest.unstable_mockModule("fs", () => ({ + default: { + existsSync: jest.fn(() => false), + writeFileSync: jest.fn(), + mkdirSync: jest.fn(), + rmSync: jest.fn(), + }, +})); + jest.unstable_mockModule("bing-translate-api", () => ({ translate: jest.fn((text, from, to) => ({ translation: `${text} translated from ${from} to ${to} using bing`, diff --git a/utils/getOrCreateJsonFile.test.js b/utils/getOrCreateJsonFile.test.js index a8e8516..d946fdf 100644 --- a/utils/getOrCreateJsonFile.test.js +++ b/utils/getOrCreateJsonFile.test.js @@ -1,35 +1,61 @@ import { parsePath } from "./getConfigPath.js"; -import { getOrCreateJsonFile } from "./getOrCreateJsonFile.js"; import fs from "fs"; +import { jest } from "@jest/globals"; + +jest.unstable_mockModule("./utils/importJSONFile.js", () => ({ + importJSONFile: jest.fn(() => ({ + languages: [ + { + name: "en", + files: ["en.json"], + }, + { + name: "es", + files: ["es.json"], + }, + ], + basePath: "test", + translationEngines: ["google", "bing", "libreTranslate"], + })), +})); + +const { importJSONFile } = await import("./importJSONFile.js"); +const { getOrCreateJsonFile } = await import("./getOrCreateJsonFile.js"); + describe("getOrCreateJsonFile", () => { let basePath, fileName, fileContent; beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + basePath = "test-configs/translations"; fileName = "en.json"; fileContent = { test: "test" }; }); - afterEach(() => { - fs.rmSync("test-configs", { recursive: true }); - }); - it("should create a new file and return the file and the parsedPath if file doesn't exists", async () => { + fs.existsSync.mockImplementation(() => false); + const { file, parsedPath } = await getOrCreateJsonFile(basePath, fileName); expect(file).toEqual({}); expect(parsedPath).toEqual(parsePath(`${basePath}/${fileName}`)); - expect(fs.existsSync(parsedPath)).toBe(true); + expect(fs.mkdirSync).toHaveBeenCalledWith(basePath, { recursive: true }); + expect(fs.writeFileSync).toHaveBeenCalledWith( + parsedPath, + JSON.stringify(file, null, 2), + ); }); it("should return the file and the parsedPath if file exists", async () => { - fs.mkdirSync(basePath, { recursive: true }); - fs.writeFileSync( - `${basePath}/${fileName}`, - JSON.stringify(fileContent, null, 2), - ); + importJSONFile.mockImplementation(() => fileContent); + fs.existsSync.mockImplementation(() => true); const { file, parsedPath } = await getOrCreateJsonFile(basePath, fileName); expect(file).toEqual(fileContent); expect(parsedPath).toEqual(parsePath(`${basePath}/${fileName}`)); + expect(fs.mkdirSync).not.toHaveBeenCalled(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(fs.existsSync).toHaveBeenCalledWith(parsedPath); }); }); diff --git a/utils/validateAndPromptUserJSONFiles.test.js b/utils/validateAndPromptUserJSONFiles.test.js index e85a1d0..06eef5e 100644 --- a/utils/validateAndPromptUserJSONFiles.test.js +++ b/utils/validateAndPromptUserJSONFiles.test.js @@ -1,21 +1,37 @@ import { jest } from "@jest/globals"; - -import { validateAndPromptUserJSONFiles } from "../utils/validateAndPromptUserJSONFiles.js"; -import { getOrCreateJsonFile } from "../utils/getOrCreateJsonFile.js"; import { parsePath } from "./getConfigPath.js"; -import promptFactory from "prompt-sync-plus"; -const prompt = promptFactory(); +import prompt from "./promptUser.js"; import fs from "fs"; -/** - * TODO: rewrite to use mocked fs instead of creating files on disk - */ -describe.skip("validateAndPromptUserJSONFiles", () => { +jest.unstable_mockModule("./utils/importJSONFile.js", () => ({ + importJSONFile: jest.fn(async () => ({})), +})); + +const { validateAndPromptUserJSONFiles } = await import( + "./validateAndPromptUserJSONFiles.js" +); +const { importJSONFile } = await import("./importJSONFile.js"); + +const mockImportJSONFile = (filesMock, basePath) => { + let filesMockWithParsedPath = {}; + Object.keys(filesMock).forEach((fileName) => { + const parsedPath = parsePath(`${basePath}/${fileName}`); + filesMockWithParsedPath[parsedPath] = filesMock[fileName]; + }); + + importJSONFile.mockImplementation(async (path) => { + return filesMockWithParsedPath[path]; + }); +}; + +describe("validateAndPromptUserJSONFiles", () => { let filesMock, filesName, basePath, nameOfTranslation; beforeEach(() => { jest.clearAllMocks(); jest.resetModules(); + fs.existsSync = jest.fn().mockReturnValue(true); + filesMock = { "withTest.json": { test: "Test translation", @@ -29,23 +45,10 @@ describe.skip("validateAndPromptUserJSONFiles", () => { basePath = "test-configs/translations"; nameOfTranslation = "test"; - - // Creating files - filesName.forEach((fileName) => { - let { file, parsedPath } = getOrCreateJsonFile(basePath, fileName); - file = filesMock[fileName]; - - fs.writeFileSync(parsedPath, JSON.stringify(file, null, 2)); - }); - }); - - afterEach(() => { - if (fs.existsSync("test-configs")) { - fs.rmSync("test-configs", { recursive: true }); - } }); it("should not include the json files that already have the property if the user doesn't confirm it", async () => { + mockImportJSONFile(filesMock, basePath); prompt.mockImplementationOnce(() => "no"); const filesToEdit = await validateAndPromptUserJSONFiles( @@ -58,13 +61,14 @@ describe.skip("validateAndPromptUserJSONFiles", () => { ); const expectedParsedPath = parsePath(`${basePath}/${fileName}`); - expect(prompt).toHaveBeenCalledTimes(1); expect(filesToEdit).toEqual([ { file: filesMock[fileName], parsedPath: expectedParsedPath }, ]); + expect(prompt).toHaveBeenCalledTimes(1); }); it("should include the json files that already have the property if the user confirm it", async () => { + mockImportJSONFile(filesMock, basePath); prompt.mockImplementationOnce(() => "yes"); const filesToEdit = await validateAndPromptUserJSONFiles( @@ -82,6 +86,7 @@ describe.skip("validateAndPromptUserJSONFiles", () => { }); it("should return an empty array if all the files already have the property and the user doesn't want overwrite them", async () => { + mockImportJSONFile(filesMock, basePath); prompt.mockImplementationOnce(() => "no"); const filesToEdit = await validateAndPromptUserJSONFiles(