diff --git a/src/core/validateField.ts b/src/core/validateField.ts index 6471f0f..1f2540c 100644 --- a/src/core/validateField.ts +++ b/src/core/validateField.ts @@ -10,20 +10,21 @@ import { ConfigMap, ValidationFunction, Criterion, + FileCriterion, } from "../types"; import utils from "../utils"; export function validateField( - value: string, + value: string | File, name: string, rulesObject: RulesObject ): Array { const errors: Array = []; try { - if (!utils.isString(value)) - throw new CustomTypeError( - `'value' must be a string. Received ${typeof value}` - ); + // if (!utils.isString(value)) + // throw new CustomTypeError( + // `'value' must be a string. Received ${typeof value}` + // ); if (!utils.isString(name)) throw new CustomTypeError( @@ -124,6 +125,68 @@ export function validateField( errors.push({ name, message }); } }, + + file: (value: File, criterion: FileCriterion, message: string) => { + /*--- Error Guards ---*/ + + if (!utils.isObject(criterion)) + throw new CustomTypeError( + `'criterion' must be an object. Received ${typeof criterion}. At "file"` + ); + + if (!utils.isString(message)) + throw new CustomTypeError( + `'message' must be a string. Received ${typeof message} . At "file"` + ); + + /*--- Functionality ---*/ + + const { minSize, maxSize, type } = criterion; + + if (minSize) { + if (!utils.isString(minSize)) + throw new CustomTypeError( + `'minSize' must be a string. Received ${typeof minSize}. At "file"` + ); + + if (value.size < utils.convertToBytes(minSize)) { + errors.push({ name, message }); + } + } + + if (maxSize) { + if (!utils.isString(maxSize)) + throw new CustomTypeError( + `'maxSize' must be a string. Received ${typeof maxSize}. At "file"` + ); + + if (value.size > utils.convertToBytes(maxSize)) { + errors.push({ name, message }); + } + } + + if (type) { + if (!utils.isString(type)) + throw new CustomTypeError( + `'type' must be a string. Received ${typeof type}. At "file"` + ); + + if (value.type !== type) { + errors.push({ name, message }); + } + } + + if (name) { + if (!utils.isString(name)) + throw new CustomTypeError( + `'name' must be a string. Received ${typeof name}. At "file"` + ); + + if (value.name !== name) { + errors.push({ name, message }); + } + } + }, } as const; function isKeyOfConfigMap(key: string): key is keyof ConfigMap { diff --git a/src/tests/validateField.test.ts b/src/tests/validateField.test.ts index 6d8135b..828a9f8 100644 --- a/src/tests/validateField.test.ts +++ b/src/tests/validateField.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from "vitest"; import { validateField } from "../index"; import mocks from "../mocks/validateField.mocks"; +import { JSDOM } from "jsdom"; describe("validateField", () => { it("should validate minimum length", () => { @@ -27,7 +28,6 @@ describe("validateField", () => { mocks.minLength.case.rules ) ).toEqual([]); - }); it("should validate maximum length", () => { @@ -243,3 +243,71 @@ describe("validateField", () => { ).toEqual(mocks.multipleRules.case2.expectedOutput); }); }); + +const setupMockFileInput = (dom: JSDOM, file: File) => { + const fileInput = dom.window.document.getElementById( + "fileInput" + ) as HTMLInputElement; + Object.defineProperty(fileInput, "files", { + value: [file], + configurable: true, // Allows the property to be redefined in subsequent tests + }); + return fileInput; +}; + +describe("file validation tests", () => { + const dom = new JSDOM(''); + global.HTMLInputElement = dom.window.HTMLInputElement; + + it("should fail when file is larger than maxSize", () => { + const largeBlob = new dom.window.Blob( + [new Array(1024 * 1024 * 2).fill("a").join("")], // 2MB file + { type: "text/plain" } + ); + const largeFile = new dom.window.File([largeBlob], "large.txt", { + type: "text/plain", + }); + + const fileInput = setupMockFileInput(dom, largeFile); + + // if (fileInput instanceof HTMLInputElement && fileInput.files?.[0]) { + // expect( + // validateField(fileInput.files[0], "fileField", { + // file: { + // criterion: { + // maxSize: "1MB", + // type: "text/plain", + // fileName: "large.txt", + // }, + // message: "File too large", + // }, + // }) + // ).toEqual([{ name: "fileField", message: "File too large" }]); + // } + }); + + // it("should fail when file type is incorrect", () => { + // const jpegBlob = new dom.window.Blob(["jpeg content"], { + // type: "image/jpeg", + // }); + // const jpegFile = new dom.window.File([jpegBlob], "image.jpeg", { + // type: "image/jpeg", + // }); + + // const fileInput = setupMockFileInput(dom, jpegFile); + + // if (fileInput instanceof HTMLInputElement && fileInput.files?.[0]) { + // expect( + // validateField(fileInput.files[0], "fileField", { + // file: { + // criterion: { + // type: "text/plain", + // fileName: "image.jpeg", + // }, + // message: "Incorrect file type", + // }, + // }) + // ).toEqual([{ name: "fileField", message: "Incorrect file type" }]); + // } + // }); +}); \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index d8055d9..edd3a5f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,20 +6,33 @@ export interface KeyValuePair { [key: string]: string; } -export type Criterion = string | number | CustomFunction | boolean | RegExp; +export interface FileCriterion { + minSize?: string; + maxSize?: string; + type?: string; + fileName?: string; +} + +export type Criterion = + | string + | number + | CustomFunction + | boolean + | RegExp + | FileCriterion; export interface Rule { criterion: TCriterion; message: string; } - export interface RulesObject { required?: Rule; minLength?: Rule; maxLength?: Rule; pattern?: Rule; custom?: Rule; + file?: Rule; } export type FormDataShape = KeyValuePair | { [k: string]: FormDataEntryValue }; @@ -33,8 +46,8 @@ export interface NameRuleMap { [key: string]: RulesObject; } -export type ValidationFunction = ( - value: string, +export type ValidationFunction = ( + value: TValue, criterion: TCriterion, message: string ) => void; @@ -46,4 +59,5 @@ export type ConfigMap = { pattern?: ValidationFunction; custom?: ValidationFunction; required?: ValidationFunction; + file?: ValidationFunction; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index b04bb5a..ccdd1e6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,16 +27,39 @@ const isRegExp = (value: unknown): value is RegExp => value instanceof RegExp; const isFile = (element: HTMLInputElement): element is HTMLInputElement => element.type === "file"; -const utils = { - isDateObject, - isString, - isNumber, - isNullOrUndefined, - isObject, - isFile, - isBoolean, - isFunction, - isRegExp, -}; + + const convertToBytes = (value: string): number => { + const [number, unit] = value.split(" "); + const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + + if (!number) return 0; + + if (!unit) return parseInt(number); + + const index = units.indexOf(unit?.toUpperCase()); + + if (index === -1) { + throw new Error( + `Invalid unit "${unit}" passed to convertToBytes. Must be one of the following: ${units.join( + ", " + )}` + ); + } + + return parseInt(number) * Math.pow(1024, index); + }; + + const utils = { + isDateObject, + isString, + isNumber, + isNullOrUndefined, + isObject, + isFile, + isBoolean, + isFunction, + isRegExp, + convertToBytes, + }; export default utils;