Skip to content

Commit

Permalink
file
Browse files Browse the repository at this point in the history
  • Loading branch information
AmmarHalees committed Jan 3, 2024
1 parent f63b444 commit 9e4ff37
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 21 deletions.
73 changes: 68 additions & 5 deletions src/core/validateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FieldError> {
const errors: Array<FieldError> = [];
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(
Expand Down Expand Up @@ -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 {
Expand Down
70 changes: 69 additions & 1 deletion src/tests/validateField.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -27,7 +28,6 @@ describe("validateField", () => {
mocks.minLength.case.rules
)
).toEqual([]);

});

it("should validate maximum length", () => {
Expand Down Expand Up @@ -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('<!DOCTYPE html><input type="file" id="fileInput">');
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);

Check failure on line 271 in src/tests/validateField.test.ts

View workflow job for this annotation

GitHub Actions / build

'fileInput' is declared but its value is never read.

// 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" }]);
// }
// });
});
22 changes: 18 additions & 4 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TCriterion = Criterion> {
criterion: TCriterion;
message: string;
}


export interface RulesObject {
required?: Rule<boolean>;
minLength?: Rule<number>;
maxLength?: Rule<number>;
pattern?: Rule<RegExp>;
custom?: Rule<CustomFunction>;
file?: Rule<FileCriterion>;
}

export type FormDataShape = KeyValuePair | { [k: string]: FormDataEntryValue };
Expand All @@ -33,8 +46,8 @@ export interface NameRuleMap {
[key: string]: RulesObject;
}

export type ValidationFunction<TCriterion> = (
value: string,
export type ValidationFunction<TCriterion, TValue = string> = (
value: TValue,
criterion: TCriterion,
message: string
) => void;
Expand All @@ -46,4 +59,5 @@ export type ConfigMap = {
pattern?: ValidationFunction<RegExp>;
custom?: ValidationFunction<CustomFunction>;
required?: ValidationFunction<boolean>;
file?: ValidationFunction<FileCriterion, File>;
};
45 changes: 34 additions & 11 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 9e4ff37

Please sign in to comment.