diff --git a/README.md b/README.md index bc5a9cc..0e5967d 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,9 @@ export default defineConfig(({ mode }): UserConfig => { ## TODO +- [x] Basic Tests. - [ ] Better Documentation. -- [ ] Add Tests. +- [ ] Tests for all files & functions. - [ ] Many More. ## Credits diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..7db5ee1 --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,440 @@ +import { describe, expect, it } from "bun:test"; + +import ci4 from "src"; +import type { Ci4Plugin } from "src/types"; + +describe("@fabithub/vite-plugin-ci4", () => { + it("handles missing configuration", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(() => ci4()).toThrow("@fabithub/vite-plugin-ci4: missing configuration."); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(() => ci4({})).toThrow( + "@fabithub/vite-plugin-ci4: missing configuration for 'input'." + ); + }); + + it("accepts a single input", () => { + const plugin = ci4("resources/js/app.ts") as Ci4Plugin[]; + + const config = plugin[0].config({}, { command: "build", mode: "production" }); + expect(config.build?.rollupOptions?.input).toBe("resources/js/app.ts"); + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.build?.rollupOptions?.input).toBe("resources/js/app.ts"); + }); + + it("accepts an array of inputs", () => { + const plugin = ci4(["resources/js/app.ts", "resources/js/other.js"]) as Ci4Plugin[]; + + const config = plugin[0].config({}, { command: "build", mode: "production" }); + expect(config.build?.rollupOptions?.input).toEqual([ + "resources/js/app.ts", + "resources/js/other.js" + ]); + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.build?.rollupOptions?.input).toEqual([ + "resources/js/app.ts", + "resources/js/other.js" + ]); + }); + + it("accepts a full configuration", () => { + const plugin = ci4({ + input: "resources/js/app.ts", + publicDirectory: "other-public", + buildDirectory: "other-build", + ssr: "resources/js/ssr.ts", + ssrOutputDirectory: "other-ssr-output" + }) as Ci4Plugin[]; + + const config = plugin[0].config({}, { command: "build", mode: "production" }); + expect(config.base).toBe("/other-build/"); + expect(config.build?.manifest).toBe("manifest.json"); + expect(config.build?.outDir).toBe("other-public/other-build"); + expect(config.build?.rollupOptions?.input).toBe("resources/js/app.ts"); + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.base).toBe("/other-build/"); + expect(ssrConfig.build?.manifest).toBe(false); + expect(ssrConfig.build?.outDir).toBe("other-ssr-output"); + expect(ssrConfig.build?.rollupOptions?.input).toBe("resources/js/ssr.ts"); + }); + + it("respects the users build.manifest config option", () => { + const plugin = ci4({ input: "resources/js/app.js" }) as Ci4Plugin[]; + + const userConfig = { build: { manifest: "my-custom-manifest.json" } }; + const config = plugin[0].config(userConfig, { command: "build", mode: "production" }); + + expect(config.build?.manifest).toBe("my-custom-manifest.json"); + }); + + it("has a default manifest path", () => { + const plugin = ci4({ input: "resources/js/app.js" }) as Ci4Plugin[]; + + const userConfig = {}; + const config = plugin[0].config(userConfig, { command: "build", mode: "production" }); + + expect(config.build?.manifest).toBe("manifest.json"); + }); + + it("respects users base config option", () => { + const plugin = ci4({ input: "resources/js/app.ts" }) as Ci4Plugin[]; + + const userConfig = { base: "/foo/" }; + const config = plugin[0].config(userConfig, { command: "build", mode: "production" }); + + expect(config.base).toBe("/foo/"); + }); + + it("accepts a partial configuration", () => { + const plugin = ci4({ + input: "resources/js/app.js", + ssr: "resources/js/ssr.js" + }) as Ci4Plugin[]; + + const config = plugin[0].config({}, { command: "build", mode: "production" }); + expect(config.base).toBe("/build/"); + expect(config.build?.manifest).toBe("manifest.json"); + expect(config.build?.outDir).toBe("public/build"); + expect(config.build?.rollupOptions?.input).toBe("resources/js/app.js"); + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.base).toBe("/build/"); + expect(ssrConfig.build?.manifest).toBe(false); + expect(ssrConfig.build?.outDir).toBe("writable/ssr"); + expect(ssrConfig.build?.rollupOptions?.input).toBe("resources/js/ssr.js"); + }); + + it("uses the default entry point when ssr entry point is not provided", () => { + // This is support users who may want a dedicated Vite config for SSR. + const plugin = ci4("resources/js/ssr.js") as Ci4Plugin[]; + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.build?.rollupOptions?.input).toBe("resources/js/ssr.js"); + }); + + it("prefixes the base with ASSET_URL in production mode", () => { + process.env["app_assetURL"] = "http://example.com"; + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + + const devConfig = plugin[0].config({}, { command: "serve", mode: "development" }); + expect(devConfig.base).toBe(""); + + const prodConfig = plugin[0].config({}, { command: "build", mode: "production" }); + expect(prodConfig.base).toBe("http://example.com/build/"); + + delete process.env["app_assetURL"]; + }); + + it("prevents setting an empty publicDirectory", () => { + expect(() => ci4({ input: "resources/js/app.js", publicDirectory: "" })).toThrow( + "@fabithub/vite-plugin-ci4: publicDirectory must be a subdirectory. E.g. 'public'." + ); + }); + + it("prevents setting an empty buildDirectory", () => { + expect(() => ci4({ input: "resources/js/app.js", buildDirectory: "" })).toThrow( + "@fabithub/vite-plugin-ci4: buildDirectory must be a subdirectory. E.g. 'build'." + ); + }); + + it("handles surrounding slashes on directories", () => { + const plugin = ci4({ + input: "resources/js/app.js", + publicDirectory: "/public/test/", + buildDirectory: "/build/test/", + ssrOutputDirectory: "/ssr-output/test/" + }) as Ci4Plugin[]; + + const config = plugin[0].config({}, { command: "build", mode: "production" }); + expect(config.base).toBe("/build/test/"); + expect(config.build?.outDir).toBe("public/test/build/test"); + + const ssrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(ssrConfig.build?.outDir).toBe("ssr-output/test"); + }); + + it("provides an @components alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@components"]).toBe("/resources/components"); + }); + + it("provides an @contexts alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@contexts"]).toBe("/resources/contexts"); + }); + + it("provides an @hooks alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@hooks"]).toBe("/resources/hooks"); + }); + + it("provides an @layouts alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@layouts"]).toBe("/resources/layouts"); + }); + + it("provides an @styles alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@styles"]).toBe("/resources/styles"); + }); + + it("provides an @utils alias by default", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config({}, { command: "build", mode: "development" }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@utils"]).toBe("/resources/utils"); + }); + + it("define new @ alias by user", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + + const config = plugin[0].config( + { resolve: { alias: { "@": "/somewhere/else" } } }, + { command: "build", mode: "development" } + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@"]).toBe("/somewhere/else"); + }); + + it("respects a users existing @components alias", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + + const config = plugin[0].config( + { resolve: { alias: { "@components": "/somewhere/else" } } }, + { command: "build", mode: "development" } + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(config.resolve?.alias?.["@components"]).toBe("/somewhere/else"); + }); + + it("appends an Alias object when using an alias array", () => { + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + const config = plugin[0].config( + { resolve: { alias: [{ find: "@components", replacement: "/something/else" }] } }, + { command: "build", mode: "development" } + ); + + expect(config.resolve?.alias).toEqual([ + { + find: "@components", + replacement: "/something/else" + }, + { + find: "@components", + replacement: "/resources/components" + }, + { + find: "@contexts", + replacement: "/resources/contexts" + }, + { + find: "@layouts", + replacement: "/resources/layouts" + }, + { + find: "@styles", + replacement: "/resources/styles" + }, + { + find: "@utils", + replacement: "/resources/utils" + }, + { + find: "@hooks", + replacement: "/resources/hooks" + }, + { + find: "types", + replacement: "/resources/types" + } + ]); + }); + + it("prevents the Inertia helpers from being externalized", () => { + /* eslint-disable @typescript-eslint/ban-ts-comment */ + const plugin = ci4("resources/js/app.js") as Ci4Plugin[]; + + const noSsrConfig = plugin[0].config( + { build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(noSsrConfig.ssr?.noExternal).toEqual(["@fabithub/vite-plugin-ci4"]); + + const nothingExternalConfig = plugin[0].config( + { ssr: { noExternal: true }, build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(nothingExternalConfig.ssr?.noExternal).toBe(true); + + const arrayNoExternalConfig = plugin[0].config( + { ssr: { noExternal: ["foo"] }, build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(arrayNoExternalConfig.ssr?.noExternal).toEqual(["foo", "@fabithub/vite-plugin-ci4"]); + + const stringNoExternalConfig = plugin[0].config( + { ssr: { noExternal: "foo" }, build: { ssr: true } }, + { command: "build", mode: "production" } + ); + expect(stringNoExternalConfig.ssr?.noExternal).toEqual([ + "foo", + "@fabithub/vite-plugin-ci4" + ]); + }); + + it("does not configure full reload when configuration it not an object", () => { + const plugins = ci4("resources/js/app.js") as Ci4Plugin[]; + + expect(plugins.length).toBe(1); + }); + + it("does not configure full reload when refresh is not present", () => { + const plugins = ci4({ input: "resources/js/app.js" }) as Ci4Plugin[]; + + expect(plugins.length).toBe(1); + }); + + it("does not configure full reload when refresh is set to undefined", () => { + const plugins = ci4({ input: "resources/js/app.js", refresh: undefined }) as Ci4Plugin[]; + expect(plugins.length).toBe(1); + }); + + it("does not configure full reload when refresh is false", () => { + const plugins = ci4({ input: "resources/js/app.js", refresh: false }) as Ci4Plugin[]; + + expect(plugins.length).toBe(1); + }); + + it("configures full reload with routes and views when refresh is true", () => { + const plugins = ci4({ input: "resources/js/app.js", refresh: true }) as Ci4Plugin[]; + + expect(plugins.length).toBe(2); + // @ts-ignore + expect(plugins[1].__ci4_plugin_config).toEqual({ + paths: [ + "app/Views/*", + "modules/**/Views/*", + "app/Config/Routes.php", + "modules/**/Config/Routes.php" + ] + }); + }); + + it("configures full reload when refresh is a single path", () => { + const plugins = ci4({ + input: "resources/js/app.js", + refresh: "path/to/watch/**" + }) as Ci4Plugin[]; + + expect(plugins.length).toBe(2); + // @ts-ignore + expect(plugins[1].__ci4_plugin_config).toEqual({ paths: ["path/to/watch/**"] }); + }); + + it("configures full reload when refresh is an array of paths", () => { + const plugins = ci4({ + input: "resources/js/app.js", + refresh: ["path/to/watch/**", "another/to/watch/**"] + }) as Ci4Plugin[]; + + expect(plugins.length).toBe(2); + // @ts-ignore + expect(plugins[1].__ci4_plugin_config).toEqual({ + paths: ["path/to/watch/**", "another/to/watch/**"] + }); + }); + + it("configures full reload when refresh is a complete configuration to proxy", () => { + const plugins = ci4({ + input: "resources/js/app.js", + refresh: { + paths: ["path/to/watch/**", "another/to/watch/**"], + config: { delay: 987 } + } + }) as Ci4Plugin[]; + + expect(plugins.length).toBe(2); + // @ts-ignore + expect(plugins[1].__ci4_plugin_config).toEqual({ + paths: ["path/to/watch/**", "another/to/watch/**"], + config: { delay: 987 } + }); + }); + + it("configures full reload when refresh is an array of complete configurations to proxy", () => { + const plugins = ci4({ + input: "resources/js/app.js", + refresh: [ + { + paths: ["path/to/watch/**"], + config: { delay: 987 } + }, + { + paths: ["another/to/watch/**"], + config: { delay: 123 } + } + ] + }) as Ci4Plugin[]; + + expect(plugins.length).toBe(3); + // @ts-ignore + expect(plugins[1].__ci4_plugin_config).toEqual({ + paths: ["path/to/watch/**"], + config: { delay: 987 } + }); + // @ts-ignore + expect(plugins[2].__ci4_plugin_config).toEqual({ + paths: ["another/to/watch/**"], + config: { delay: 123 } + }); + }); +}); diff --git a/tests/plugins/ci4.test.ts b/tests/plugins/ci4.test.ts new file mode 100644 index 0000000..e25d1f4 --- /dev/null +++ b/tests/plugins/ci4.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "bun:test"; +import { ci4 } from "@plugins/ci4"; + +describe("ci4 plugin", () => { + it("should be a function", () => { + expect(ci4).toBeFunction(); + }); + + describe("returned object", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const plugin = ci4({}); + + it("should have an enforce property", () => { + expect(plugin.enforce).toBeString(); + }); + + it("should have a name property", () => { + expect(plugin.name).toBeString(); + }); + + it("should have a config property", () => { + expect(plugin.config).toBeFunction(); + }); + + it("should have a configResolved property", () => { + expect(plugin.configResolved).toBeFunction(); + }); + + it("should have a transform property", () => { + expect(plugin.transform).toBeFunction(); + }); + + it("should have a configureServer property", () => { + expect(plugin.configureServer).toBeFunction(); + }); + }); +}); diff --git a/tests/utils/bun.test.ts b/tests/utils/bun.test.ts new file mode 100644 index 0000000..235fb6c --- /dev/null +++ b/tests/utils/bun.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "bun:test"; +import { isBunRunning } from "@utils/bun"; + +describe("Bun Test", () => { + it("isBunRunning() function returns true when running in Bun", () => { + it.skipIf(isBunRunning())("isBunRunning() returns false when running not in Bun", () => { + expect(isBunRunning()).toBeFalse(); + }); + + expect(isBunRunning()).toBeTrue(); + }); +}); diff --git a/tests/utils/decorate.test.ts b/tests/utils/decorate.test.ts new file mode 100644 index 0000000..96f5222 --- /dev/null +++ b/tests/utils/decorate.test.ts @@ -0,0 +1,16 @@ +import colors from "picocolors"; +import { describe, expect, it } from "bun:test"; +import { highlighter } from "@utils/decorate"; + +describe("String Functions", () => { + describe("highlighter", () => { + it("should return highlight a string", () => { + const plugin = { name: "test", version: "1.0.0" }; + + const highlightedVersion = highlighter(plugin); + expect(highlightedVersion).toBe( + ` ${colors.green("➜")} ${colors.white(plugin.name)}: ${colors.cyan(plugin.version)}` + ); + }); + }); +}); diff --git a/tests/utils/errors.test.ts b/tests/utils/errors.test.ts new file mode 100644 index 0000000..27f5c5f --- /dev/null +++ b/tests/utils/errors.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "bun:test"; +import { Errors, errorMessages } from "@utils/errors"; + +describe("Error Messages", () => { + const pluginName = "@fabithub/vite-plugin-ci4"; + + it("should return an error message for InvalidConfiguration", () => { + const message = errorMessages(Errors.InvalidConfiguration); + const expected = pluginName + ": missing configuration."; + expect(message).toBe(expected); + }); + + it("should return an error message for InvalidInput", () => { + const message = errorMessages(Errors.InvalidInput); + const expected = pluginName + ": missing configuration for 'input'."; + expect(message).toBe(expected); + }); + + it("should return an error message for InvalidBuildSubDirectory", () => { + const message = errorMessages(Errors.InvalidBuildSubDirectory); + const expected = pluginName + ": buildDirectory must be a subdirectory. E.g. 'build'."; + expect(message).toBe(expected); + }); + + it("should return an error message for InvalidPublicSubDirectory", () => { + const message = errorMessages(Errors.InvalidPublicSubDirectory); + const expected = pluginName + ": publicDirectory must be a subdirectory. E.g. 'public'."; + expect(message).toBe(expected); + }); +}); diff --git a/tests/utils/io.test.ts b/tests/utils/io.test.ts new file mode 100644 index 0000000..8a6b24a --- /dev/null +++ b/tests/utils/io.test.ts @@ -0,0 +1,186 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn, mock } from "bun:test"; +import { isFileExists, readFileAsJson, readFileAsString, removeFile, writingFile } from "@utils/io"; + +const existsTextFile = "exists-file.txt"; +const nonExistsFile = "non-exists-file.txt"; +const existsJsonFile = "exists-json-file.txt"; +const existsNumberFile = "exists-number-file.txt"; +const existsAnotherTypeFile = "exists-another-type-file.txt"; + +const numberContent = 4.57415678525; +const jsonContent = { name: "something", version: "1.0.0" }; +const stringContent = "this is a txt file with dummy content."; + +describe("IO Functions", () => { + afterEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + process.versions.bun = Bun.version; + mock.restore(); + }); + + describe("Bun Runtime", () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation((filePath: string) => ({ + exists: () => { + if (filePath.includes("non-")) { + return Promise.resolve(false); + } + return Promise.resolve(true); + }, + text: () => { + if (filePath.includes("json")) { + return Promise.resolve(jsonContent); + } else if (filePath.includes("number")) { + return Promise.resolve(numberContent); + } + return Promise.resolve(stringContent); + } + })); + + spyOn(global.Bun, "write").mockResolvedValue(45); + + mock.module("fs/promises", () => ({ rm: () => Promise.resolve() })); + }); + + describe("isFileExists", () => { + it("should return true if the file exists", async () => + expect(await isFileExists(existsTextFile)).toBeTrue()); + + it("should return false if the file does not exists", async () => + expect(await isFileExists(nonExistsFile)).toBeFalse()); + }); + + describe("readFileAsString", () => { + it("should read a text file or other type of file as string", async () => { + expect(await readFileAsString(existsJsonFile)).toBeString(); + expect(await readFileAsString(existsNumberFile)).toBeString(); + expect(await readFileAsString(existsTextFile)).toBe(stringContent); + }); + + it("should throw an error if the file does not exists", () => + expect(async () => await readFileAsString(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + + describe("readFileAsJson", () => { + it("should read and parse a JSON file", async () => + expect(await readFileAsJson(existsJsonFile)).toEqual(jsonContent)); + + it("should throw an error if the file does not a valid json file", () => + expect(async () => await readFileAsJson(existsAnotherTypeFile)).toThrow( + "It is not a valid Json file." + )); + + it("should throw an error if the file does not exists", () => + expect(async () => await readFileAsJson(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + + describe("writingFile", () => { + it("should write string on existing or non-existing file", async () => { + expect(await writingFile(nonExistsFile, stringContent)).toBeTrue(); + expect(await writingFile(existsTextFile, stringContent)).toBeTrue(); + }); + }); + + describe("removeFile", () => { + it("should remove existing file", async () => + expect(await removeFile(existsTextFile)).toBeTrue()); + + it("should throw an error if the file does not exists", () => + expect(async () => await removeFile(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + }); + + describe("Node Runtime", () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + process.versions.bun = undefined; + mock.module("fs/promises", () => ({ + rm: () => Promise.resolve(), + mkdir: () => Promise.resolve(), + lstat: () => Promise.resolve(), + readdir: () => Promise.resolve(), + default: () => Promise.resolve(), + realpath: () => Promise.resolve(), + writeFile: () => Promise.resolve(), + constants: () => Promise.resolve(), + readFile: (filePath: string) => { + if (filePath.includes("json")) { + return Promise.resolve(jsonContent); + } else if (filePath.includes("number")) { + return Promise.resolve(numberContent); + } + return Promise.resolve(stringContent); + }, + access: async (filePath: string) => { + if (filePath.includes("non-")) { + return Promise.resolve(false); + } + return; + } + })); + }); + + describe("isFileExists", () => { + it("should return true if the file exists", async () => + expect(await isFileExists(existsTextFile)).toBeTrue()); + + it("should return false if the file does not exist", async () => + expect(await isFileExists(nonExistsFile)).toBeFalse()); + }); + + describe("readFileAsString", () => { + it("should read a text file or other type of file as string", async () => { + expect(await readFileAsString(existsJsonFile)).toBeString(); + expect(await readFileAsString(existsNumberFile)).toBeString(); + expect(await readFileAsString(existsTextFile)).toBe(stringContent); + }); + + it("should throw an error if the file does not exists", () => + expect(async () => await readFileAsString(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + + describe("readFileAsJson", () => { + it("should read and parse a JSON file", async () => + expect(await readFileAsJson(existsJsonFile)).toEqual(jsonContent)); + + it("should throw an error if the file does not a valid json file", () => + expect(async () => await readFileAsJson(existsAnotherTypeFile)).toThrow( + "It is not a valid Json file." + )); + + it("should throw an error if the file does not exists", () => + expect(async () => await readFileAsJson(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + + describe("writingFile", () => { + it("should write string on existing or non-existing file", async () => { + expect(await writingFile(nonExistsFile, stringContent)).toBeTrue(); + expect(await writingFile(existsTextFile, stringContent)).toBeTrue(); + }); + }); + + describe("removeFile", () => { + it("should remove existing file", async () => + expect(await removeFile(existsTextFile)).toBeTrue()); + + it("should throw an error if the file does not exists", () => + expect(async () => await removeFile(nonExistsFile)).toThrow( + nonExistsFile + " not found." + )); + }); + }); +}); diff --git a/tests/utils/string.test.ts b/tests/utils/string.test.ts new file mode 100644 index 0000000..4d31867 --- /dev/null +++ b/tests/utils/string.test.ts @@ -0,0 +1,43 @@ +import { join } from "path"; +import { describe, it, expect } from "bun:test"; + +export const joinPaths = (...paths: string[]): string => join(...paths); + +describe("joinPaths", () => { + it("should join multiple paths", () => { + const result1 = joinPaths("path1", "path2"); + expect(result1).toEqual("path1/path2"); + + const result2 = joinPaths("path1", "path2", "path3"); + expect(result2).toEqual("path1/path2/path3"); + }); + + it("should ignore empty paths", () => { + const result = joinPaths("path1", "", "path2", "", "path3"); + expect(result).toEqual("path1/path2/path3"); + }); + + it("should return the first path if only one path is provided", () => { + const result = joinPaths("path1"); + expect(result).toEqual("path1"); + }); + + it("should handle absolute paths", () => { + const result1 = joinPaths("/path1", "path2"); + expect(result1).toEqual("/path1/path2"); + + const result2 = joinPaths("path1", "/path2", "path3"); + expect(result2).toEqual("path1/path2/path3"); + }); + + it("should handle paths with trailing slashes", () => { + const result1 = joinPaths("path1/", "path2"); + expect(result1).toEqual("path1/path2"); + + const result2 = joinPaths("path1", "path2/", "path3"); + expect(result2).toEqual("path1/path2/path3"); + + const result3 = joinPaths("path1/", "path2/", "path3/"); + expect(result3).toEqual("path1/path2/path3/"); + }); +}); diff --git a/tests/utils/uri.test.ts b/tests/utils/uri.test.ts new file mode 100644 index 0000000..eab0fe0 --- /dev/null +++ b/tests/utils/uri.test.ts @@ -0,0 +1,58 @@ +import type { AddressInfo } from "net"; +import { describe, it, expect } from "bun:test"; + +import type { IsAddressInfo } from "src/types"; +import { addSlash, getCurrentPath, isIpv6 } from "@utils/uri"; + +describe("URI Helper", () => { + describe("isAddressInfo", () => { + it("should return true for an AddressInfo object", () => { + const addressInfo: AddressInfo = { address: "127.0.0.1", family: "IPv4", port: 80 }; + const isAddressInfo: IsAddressInfo = (x): x is AddressInfo => typeof x === "object"; + const result = isAddressInfo(addressInfo); + + expect(result).toBeTrue(); + }); + + it("should return false for a non-AddressInfo object", () => { + const notAddressInfo = 123; + const isAddressInfo: IsAddressInfo = (x): x is AddressInfo => typeof x === "object"; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const result = isAddressInfo(notAddressInfo); + + expect(result).toBeFalse(); + }); + }); + + describe("isIpv6", () => { + it("should return true for IPv6 addresses", () => { + expect(isIpv6({ address: "::1", port: 8080, family: "IPv6" })).toBeTrue(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(isIpv6({ address: "::1", family: 6, port: 8080 })).toBeTrue(); + }); + + it("should return false for IPv4 addresses", () => { + expect(isIpv6({ address: "127.0.0.1", family: "IPv4", port: 8080 })).toBeFalse(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(isIpv6({ address: "127.0.0.1", family: 4, port: 8080 })).toBeFalse(); + }); + }); + + it("should return correct current path", () => { + expect(getCurrentPath()).toBe(process.cwd() + "/src/utils/"); + }); + + describe("addSlash", () => { + it("should add a slash to the end of a path if it doesn't already have one", () => { + expect(addSlash("/path/to/directory")).toBe("/path/to/directory/"); + }); + + it("should not add a slash to the end of a path if it already has one", () => { + expect(addSlash("/path/to/directory/")).toBe("/path/to/directory/"); + }); + }); +}); diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts new file mode 100644 index 0000000..5e10ec6 --- /dev/null +++ b/tests/utils/version.test.ts @@ -0,0 +1,181 @@ +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; + +import { appConfig } from "@config/constant"; +import { getFrameworkVersion, getPluginVersion } from "@utils/version"; + +describe("Version Functions", () => { + afterEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + process.versions.bun = Bun.version; + mock.restore(); + }); + + describe("Bun Runtime", () => { + describe("getFrameworkVersion", () => { + it("should return object with framework name & version.", async () => { + const framework = { + name: appConfig.framework, + version: appConfig.frameworkCompatibleVersion + }; + const composerLock = { ...framework, packages: [framework] }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(true), + text: () => Promise.resolve(JSON.stringify(composerLock)) + })); + + expect(await getFrameworkVersion()).toEqual(framework); + }); + + it("should throw an error If installed codeigniter 4 is not compatible.", () => { + const framework = { name: appConfig.framework, version: "1.0.0" }; + const composerLock = { ...framework, packages: [framework] }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(true), + text: () => Promise.resolve(JSON.stringify(composerLock)) + })); + + expect(async () => await getFrameworkVersion()).toThrow("not compatible"); + }); + + it("should throw an error If codeigniter 4 is not found in composer.", () => { + const framework = { name: "something", version: "1.0.0" }; + const composerLock = { ...framework, packages: [framework] }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(true), + text: () => Promise.resolve(JSON.stringify(composerLock)) + })); + + expect(async () => await getFrameworkVersion()).toThrow( + "@fabithub/vite-plugin-ci4: codeigniter4/framework not found in composer.lock." + ); + }); + + it("should throw an error If composer.lock not found.", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(false) + })); + + expect(async () => await getFrameworkVersion()).toThrow("composer.lock not found."); + }); + }); + + describe("getPluginVersion", () => { + it("should return object with plugin name & version.", async () => { + const plugin = { name: appConfig.pluginName, version: "1.0.0" }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(true), + text: () => Promise.resolve(JSON.stringify(plugin)) + })); + + expect(await getPluginVersion()).toEqual(plugin); + }); + + it("should throw an error If package.json not found.", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + spyOn(global.Bun, "file").mockImplementation(() => ({ + exists: () => Promise.resolve(false) + })); + + expect(async () => await getPluginVersion()).toThrow("package.json not found."); + }); + }); + }); + + describe("Node Runtime", () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + process.versions.bun = undefined; + }); + + describe("getFrameworkVersion", () => { + it("should return object with framework name & version.", async () => { + const framework = { + name: appConfig.framework, + version: appConfig.frameworkCompatibleVersion + }; + const composerLock = { ...framework, packages: [framework] }; + + mock.module("fs/promises", () => ({ + readFile: () => Promise.resolve(JSON.stringify(composerLock)), + access: () => Promise.resolve() + })); + + expect(await getFrameworkVersion()).toEqual(framework); + }); + + it("should throw an error If installed codeigniter 4 is not compatible.", () => { + const framework = { name: appConfig.framework, version: "1.0.0" }; + const composerLock = { ...framework, packages: [framework] }; + + mock.module("fs/promises", () => ({ + readFile: () => Promise.resolve(JSON.stringify(composerLock)), + access: () => Promise.resolve() + })); + + expect(async () => await getFrameworkVersion()).toThrow( + "CompatibilityError: codeigniter4/framework@1.0.0 is not compatible with @fabithub/vite-plugin-ci4. Use CodeIgniter@4.1.5" + ); + }); + + it("should throw an error If codeigniter 4 is not found in composer.", () => { + const framework = { name: "something", version: "1.0.0" }; + const composerLock = { ...framework, packages: [framework] }; + + mock.module("fs/promises", () => ({ + readFile: () => Promise.resolve(JSON.stringify(composerLock)), + access: () => Promise.resolve() + })); + + expect(async () => await getFrameworkVersion()).toThrow( + "@fabithub/vite-plugin-ci4: codeigniter4/framework not found in composer.lock." + ); + }); + + it("should throw an error If composer.lock not found.", () => { + mock.module("fs/promises", () => ({ + access: () => Promise.reject() + })); + + expect(async () => await getFrameworkVersion()).toThrow("composer.lock not found."); + }); + }); + + describe("getPluginVersion", () => { + it("should return object with plugin name & version.", async () => { + const plugin = { name: appConfig.pluginName, version: "1.0.0" }; + + mock.module("fs/promises", () => ({ + readFile: () => Promise.resolve(JSON.stringify(plugin)), + access: () => Promise.resolve() + })); + + expect(await getPluginVersion()).toEqual(plugin); + }); + + it("should throw an error If package.json not found.", () => { + mock.module("fs/promises", () => ({ + access: () => Promise.reject() + })); + + expect(async () => await getPluginVersion()).toThrow("package.json not found."); + }); + }); + }); +});