Skip to content

Commit

Permalink
Merge pull request #37 from esamattis/esamattis/add-file-value-support
Browse files Browse the repository at this point in the history
Add File value support
  • Loading branch information
esamattis committed Mar 25, 2023
2 parents 7a50d00 + 9c116a8 commit 12a63fe
Show file tree
Hide file tree
Showing 6 changed files with 2,395 additions and 1,180 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"pnpm": {
"overrides": {
"prettier": "2.5.1",
"zod": "3.19.1",
"zod": "3.21.4",
"typescript": "4.5.5",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/react-zorm/__tests__/parse-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ describe("with any", () => {
things: [undefined, { ding: "dong" }],
});
});

test("can handle files ", () => {
const form = new FormData();
const file = new File(["(⌐□_□)"], "chucknorris.txt", {
type: "text/plain",
});
form.append("myFile", file);

const res = parseFormAny(form);

expect(res).toEqual({
myFile: file,
});

expect(res.myFile).toBe(file);
});
});

describe("combine chains with parsing", () => {
Expand Down
132 changes: 132 additions & 0 deletions packages/react-zorm/__tests__/use-zorm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,49 @@ import { useZorm } from "../src";
import { assertNotAny } from "./test-helpers";
import { createCustomIssues } from "../src/chains";

/**
* For https://github.com/testing-library/user-event/pull/1109
*/
class WorkaroundFormData extends FormData {
#formRef?: HTMLFormElement;
constructor(...args: ConstructorParameters<typeof FormData>) {
super(...args);
this.#formRef = args[0];
}

// React Zorm only uses entries() so this is the only method we need to patch
override *entries() {
for (const [name, value] of super.entries()) {
const entry: [string, FormDataEntryValue] = [name, value];

if (value instanceof File && this.#formRef) {
const input = this.#formRef.querySelector(
`input[name="${name}"]`,
);

if (input instanceof HTMLInputElement) {
const realFile = input?.files?.[0];
if (realFile) {
entry[1] = realFile;
}
}
}

yield entry;
}
}
}

const OrigFormData = globalThis.FormData;

beforeAll(() => {
globalThis.FormData = WorkaroundFormData;
});

afterAll(() => {
globalThis.FormData = OrigFormData;
});

test("single field validation", () => {
const Schema = z.object({
thing: z.string().min(1),
Expand Down Expand Up @@ -904,3 +947,92 @@ test.skip("[TYPE ONLY] can narrow validation type to success", () => {
}
}
});

test("can validate files", async () => {
const refineSpy = jest.fn();

const Schema = z.object({
myFile: z.instanceof(File).refine((file) => {
refineSpy(file.type);
return file.type === "image/png";
}, "Only .png images are allowed"),
});

function Test() {
const zo = useZorm("form", Schema);

return (
<form ref={zo.ref} data-testid="form">
<input
data-testid="file"
type="file"
name={zo.fields.myFile()}
/>

{zo.errors.myFile((e) => (
<div data-testid="error">{e.message}</div>
))}
</form>
);
}

render(<Test />);

const file = new File(["(⌐□_□)"], "chucknorris.txt", {
type: "text/plain",
});

const fileInput = screen.getByTestId("file") as HTMLInputElement;
await userEvent.upload(fileInput, file);
fireEvent.submit(screen.getByTestId("form"));

expect(refineSpy).toHaveBeenCalledWith("text/plain");

expect(screen.queryByTestId("error")).toHaveTextContent(
"Only .png images are allowed",
);
});

test("can submit files", async () => {
const submitSpy = jest.fn();

const Schema = z.object({
myFile: z.instanceof(File).refine((file) => {
return file.type === "image/png";
}, "Only .png images are allowed"),
});

function Test() {
const zo = useZorm("form", Schema, {
onValidSubmit(e) {
submitSpy(e.data.myFile.name);
},
});

return (
<form ref={zo.ref} data-testid="form">
<input
data-testid="file"
type="file"
name={zo.fields.myFile()}
/>

{zo.errors.myFile((e) => (
<div data-testid="error">{e.message}</div>
))}
</form>
);
}

render(<Test />);

const file = new File(["(⌐□_□)"], "chucknorris.png", {
type: "image/png",
});

const fileInput = screen.getByTestId("file") as HTMLInputElement;
await userEvent.upload(fileInput, file);
fireEvent.submit(screen.getByTestId("form"));

expect(submitSpy).toHaveBeenCalledWith("chucknorris.png");
});
50 changes: 25 additions & 25 deletions packages/react-zorm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,39 +44,39 @@
"dist"
],
"devDependencies": {
"@babel/core": "^7.19.3",
"@babel/preset-env": "^7.19.4",
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@playwright/test": "^1.27.1",
"@size-limit/preset-small-lib": "^8.1.0",
"@testing-library/dom": "^8.19.0",
"@babel/preset-typescript": "^7.21.0",
"@playwright/test": "^1.32.1",
"@size-limit/preset-small-lib": "^8.2.4",
"@testing-library/dom": "^9.2.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.1.2",
"@types/node": "^18.11.0",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.9",
"@types/react": "18.0.29",
"@types/react-dom": "18.0.11",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "5.40.0",
"@typescript-eslint/parser": "5.40.0",
"@valu/assert": "^1.3.1",
"babel-jest": "^29.2.0",
"esbuild": "^0.15.11",
"eslint": "8.25.0",
"@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.56.0",
"@valu/assert": "^1.3.3",
"babel-jest": "^29.5.0",
"esbuild": "^0.17.13",
"eslint": "8.36.0",
"eslint-plugin-react-hooks": "4.6.0",
"jest": "^29.2.0",
"jest-environment-jsdom": "^29.2.0",
"msw": "^0.47.4",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"msw": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "2.7.1",
"prettier": "2.8.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"size-limit": "^8.1.0",
"typescript": "4.8.4",
"vite": "^3.1.8",
"zod": "3.19.1"
"size-limit": "^8.2.4",
"typescript": "5.0.2",
"vite": "^4.2.1",
"zod": "3.21.4"
},
"size-limit": [
{
Expand Down
4 changes: 3 additions & 1 deletion packages/react-zorm/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SafeParseReturnType, ZodCustomIssue, ZodIssue, ZodType } from "zod";

type Primitive = string | number | boolean | bigint | symbol | undefined | null;

export type DeepNonNullable<T> = T extends Primitive | Date
export type DeepNonNullable<T> = T extends Primitive | Date | File
? NonNullable<T>
: T extends {}
? { [K in keyof T]-?: DeepNonNullable<T[K]> }
Expand Down Expand Up @@ -43,6 +43,8 @@ export type FieldChain<T extends object> = {
: FieldChain<T[P][0]>
: T[P] extends Date
? FieldGetter
: T[P] extends File
? FieldGetter
: T[P] extends object
? FieldChain<T[P]>
: FieldGetter;
Expand Down
Loading

1 comment on commit 12a63fe

@vercel
Copy link

@vercel vercel bot commented on 12a63fe Mar 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

react-zorm – ./

react-zorm-git-master-esamatti.vercel.app
react-zorm-esamatti.vercel.app
react-zorm.vercel.app

Please sign in to comment.