Skip to content

Commit 12a63fe

Browse files
authored
Merge pull request #37 from esamattis/esamattis/add-file-value-support
Add File value support
2 parents 7a50d00 + 9c116a8 commit 12a63fe

File tree

6 files changed

+2395
-1180
lines changed

6 files changed

+2395
-1180
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"pnpm": {
66
"overrides": {
77
"prettier": "2.5.1",
8-
"zod": "3.19.1",
8+
"zod": "3.21.4",
99
"typescript": "4.5.5",
1010
"react": "18.2.0",
1111
"react-dom": "18.2.0",

packages/react-zorm/__tests__/parse-form.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ describe("with any", () => {
155155
things: [undefined, { ding: "dong" }],
156156
});
157157
});
158+
159+
test("can handle files ", () => {
160+
const form = new FormData();
161+
const file = new File(["(⌐□_□)"], "chucknorris.txt", {
162+
type: "text/plain",
163+
});
164+
form.append("myFile", file);
165+
166+
const res = parseFormAny(form);
167+
168+
expect(res).toEqual({
169+
myFile: file,
170+
});
171+
172+
expect(res.myFile).toBe(file);
173+
});
158174
});
159175

160176
describe("combine chains with parsing", () => {

packages/react-zorm/__tests__/use-zorm.test.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,49 @@ import { useZorm } from "../src";
88
import { assertNotAny } from "./test-helpers";
99
import { createCustomIssues } from "../src/chains";
1010

11+
/**
12+
* For https://github.com/testing-library/user-event/pull/1109
13+
*/
14+
class WorkaroundFormData extends FormData {
15+
#formRef?: HTMLFormElement;
16+
constructor(...args: ConstructorParameters<typeof FormData>) {
17+
super(...args);
18+
this.#formRef = args[0];
19+
}
20+
21+
// React Zorm only uses entries() so this is the only method we need to patch
22+
override *entries() {
23+
for (const [name, value] of super.entries()) {
24+
const entry: [string, FormDataEntryValue] = [name, value];
25+
26+
if (value instanceof File && this.#formRef) {
27+
const input = this.#formRef.querySelector(
28+
`input[name="${name}"]`,
29+
);
30+
31+
if (input instanceof HTMLInputElement) {
32+
const realFile = input?.files?.[0];
33+
if (realFile) {
34+
entry[1] = realFile;
35+
}
36+
}
37+
}
38+
39+
yield entry;
40+
}
41+
}
42+
}
43+
44+
const OrigFormData = globalThis.FormData;
45+
46+
beforeAll(() => {
47+
globalThis.FormData = WorkaroundFormData;
48+
});
49+
50+
afterAll(() => {
51+
globalThis.FormData = OrigFormData;
52+
});
53+
1154
test("single field validation", () => {
1255
const Schema = z.object({
1356
thing: z.string().min(1),
@@ -904,3 +947,92 @@ test.skip("[TYPE ONLY] can narrow validation type to success", () => {
904947
}
905948
}
906949
});
950+
951+
test("can validate files", async () => {
952+
const refineSpy = jest.fn();
953+
954+
const Schema = z.object({
955+
myFile: z.instanceof(File).refine((file) => {
956+
refineSpy(file.type);
957+
return file.type === "image/png";
958+
}, "Only .png images are allowed"),
959+
});
960+
961+
function Test() {
962+
const zo = useZorm("form", Schema);
963+
964+
return (
965+
<form ref={zo.ref} data-testid="form">
966+
<input
967+
data-testid="file"
968+
type="file"
969+
name={zo.fields.myFile()}
970+
/>
971+
972+
{zo.errors.myFile((e) => (
973+
<div data-testid="error">{e.message}</div>
974+
))}
975+
</form>
976+
);
977+
}
978+
979+
render(<Test />);
980+
981+
const file = new File(["(⌐□_□)"], "chucknorris.txt", {
982+
type: "text/plain",
983+
});
984+
985+
const fileInput = screen.getByTestId("file") as HTMLInputElement;
986+
await userEvent.upload(fileInput, file);
987+
fireEvent.submit(screen.getByTestId("form"));
988+
989+
expect(refineSpy).toHaveBeenCalledWith("text/plain");
990+
991+
expect(screen.queryByTestId("error")).toHaveTextContent(
992+
"Only .png images are allowed",
993+
);
994+
});
995+
996+
test("can submit files", async () => {
997+
const submitSpy = jest.fn();
998+
999+
const Schema = z.object({
1000+
myFile: z.instanceof(File).refine((file) => {
1001+
return file.type === "image/png";
1002+
}, "Only .png images are allowed"),
1003+
});
1004+
1005+
function Test() {
1006+
const zo = useZorm("form", Schema, {
1007+
onValidSubmit(e) {
1008+
submitSpy(e.data.myFile.name);
1009+
},
1010+
});
1011+
1012+
return (
1013+
<form ref={zo.ref} data-testid="form">
1014+
<input
1015+
data-testid="file"
1016+
type="file"
1017+
name={zo.fields.myFile()}
1018+
/>
1019+
1020+
{zo.errors.myFile((e) => (
1021+
<div data-testid="error">{e.message}</div>
1022+
))}
1023+
</form>
1024+
);
1025+
}
1026+
1027+
render(<Test />);
1028+
1029+
const file = new File(["(⌐□_□)"], "chucknorris.png", {
1030+
type: "image/png",
1031+
});
1032+
1033+
const fileInput = screen.getByTestId("file") as HTMLInputElement;
1034+
await userEvent.upload(fileInput, file);
1035+
fireEvent.submit(screen.getByTestId("form"));
1036+
1037+
expect(submitSpy).toHaveBeenCalledWith("chucknorris.png");
1038+
});

packages/react-zorm/package.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,39 +44,39 @@
4444
"dist"
4545
],
4646
"devDependencies": {
47-
"@babel/core": "^7.19.3",
48-
"@babel/preset-env": "^7.19.4",
47+
"@babel/core": "^7.21.3",
48+
"@babel/preset-env": "^7.20.2",
4949
"@babel/preset-react": "^7.18.6",
50-
"@babel/preset-typescript": "^7.18.6",
51-
"@playwright/test": "^1.27.1",
52-
"@size-limit/preset-small-lib": "^8.1.0",
53-
"@testing-library/dom": "^8.19.0",
50+
"@babel/preset-typescript": "^7.21.0",
51+
"@playwright/test": "^1.32.1",
52+
"@size-limit/preset-small-lib": "^8.2.4",
53+
"@testing-library/dom": "^9.2.0",
5454
"@testing-library/jest-dom": "^5.16.5",
55-
"@testing-library/react": "^13.4.0",
55+
"@testing-library/react": "^14.0.0",
5656
"@testing-library/user-event": "^14.4.3",
57-
"@types/jest": "^29.1.2",
58-
"@types/node": "^18.11.0",
59-
"@types/react": "18.0.21",
60-
"@types/react-dom": "18.0.6",
57+
"@types/jest": "^29.5.0",
58+
"@types/node": "^18.15.9",
59+
"@types/react": "18.0.29",
60+
"@types/react-dom": "18.0.11",
6161
"@types/testing-library__jest-dom": "^5.14.5",
62-
"@typescript-eslint/eslint-plugin": "5.40.0",
63-
"@typescript-eslint/parser": "5.40.0",
64-
"@valu/assert": "^1.3.1",
65-
"babel-jest": "^29.2.0",
66-
"esbuild": "^0.15.11",
67-
"eslint": "8.25.0",
62+
"@typescript-eslint/eslint-plugin": "5.56.0",
63+
"@typescript-eslint/parser": "5.56.0",
64+
"@valu/assert": "^1.3.3",
65+
"babel-jest": "^29.5.0",
66+
"esbuild": "^0.17.13",
67+
"eslint": "8.36.0",
6868
"eslint-plugin-react-hooks": "4.6.0",
69-
"jest": "^29.2.0",
70-
"jest-environment-jsdom": "^29.2.0",
71-
"msw": "^0.47.4",
69+
"jest": "^29.5.0",
70+
"jest-environment-jsdom": "^29.5.0",
71+
"msw": "^1.2.1",
7272
"npm-run-all": "^4.1.5",
73-
"prettier": "2.7.1",
73+
"prettier": "2.8.7",
7474
"react": "18.2.0",
7575
"react-dom": "18.2.0",
76-
"size-limit": "^8.1.0",
77-
"typescript": "4.8.4",
78-
"vite": "^3.1.8",
79-
"zod": "3.19.1"
76+
"size-limit": "^8.2.4",
77+
"typescript": "5.0.2",
78+
"vite": "^4.2.1",
79+
"zod": "3.21.4"
8080
},
8181
"size-limit": [
8282
{

packages/react-zorm/src/types.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SafeParseReturnType, ZodCustomIssue, ZodIssue, ZodType } from "zod";
22

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

5-
export type DeepNonNullable<T> = T extends Primitive | Date
5+
export type DeepNonNullable<T> = T extends Primitive | Date | File
66
? NonNullable<T>
77
: T extends {}
88
? { [K in keyof T]-?: DeepNonNullable<T[K]> }
@@ -43,6 +43,8 @@ export type FieldChain<T extends object> = {
4343
: FieldChain<T[P][0]>
4444
: T[P] extends Date
4545
? FieldGetter
46+
: T[P] extends File
47+
? FieldGetter
4648
: T[P] extends object
4749
? FieldChain<T[P]>
4850
: FieldGetter;

0 commit comments

Comments
 (0)