Skip to content

Commit 7ee2f1b

Browse files
committed
test(models): unit test z.check and z.function helpers
1 parent b9fce04 commit 7ee2f1b

2 files changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type z from 'zod';
2+
import type { Audit } from '../audit.js';
3+
import type { CategoryRef } from '../category-config.js';
4+
import {
5+
createCheck,
6+
createDuplicateSlugsCheck,
7+
createDuplicatesCheck,
8+
} from './checks.js';
9+
10+
describe('createCheck', () => {
11+
it('should add issue if callback finds an error', () => {
12+
const findErrorFn = vi
13+
.fn<[string]>()
14+
.mockReturnValue({ message: 'Something went wrong' });
15+
16+
const check = createCheck(findErrorFn);
17+
18+
expect(findErrorFn).not.toHaveBeenCalled();
19+
20+
const ctx: z.core.ParsePayload<string> = { value: 'XYZ', issues: [] };
21+
check(ctx);
22+
23+
expect(findErrorFn).toHaveBeenCalledWith('XYZ');
24+
expect(ctx.issues).toEqual([
25+
{
26+
code: 'custom',
27+
message: 'Something went wrong',
28+
input: 'XYZ',
29+
},
30+
]);
31+
});
32+
33+
it('should NOT add issue if callback finds no error', () => {
34+
const findErrorFn = vi.fn<[string]>().mockReturnValue(false);
35+
36+
const check = createCheck<string>(findErrorFn);
37+
38+
expect(findErrorFn).not.toHaveBeenCalled();
39+
40+
const ctx: z.core.ParsePayload<string> = { value: 'XYZ', issues: [] };
41+
check(ctx);
42+
43+
expect(findErrorFn).toHaveBeenCalledWith('XYZ');
44+
expect(ctx.issues).toEqual([]);
45+
});
46+
});
47+
48+
describe('createDuplicatesCheck', () => {
49+
const keyFn = vi.fn(
50+
({ type, plugin, slug }: CategoryRef) => `${type} ${plugin}/${slug}`,
51+
);
52+
const errorMsgFn = vi.fn(
53+
(duplicates: string[]) => `Duplicate refs found: ${duplicates.join(', ')}`,
54+
);
55+
56+
it('add issue with custom message if there are duplicate keys', () => {
57+
const check = createDuplicatesCheck<CategoryRef>(keyFn, errorMsgFn);
58+
59+
expect(keyFn).not.toHaveBeenCalled();
60+
expect(errorMsgFn).not.toHaveBeenCalled();
61+
62+
const ctx: z.core.ParsePayload<CategoryRef[]> = {
63+
value: [
64+
{ type: 'audit', plugin: 'coverage', slug: 'coverage', weight: 2 },
65+
{ type: 'audit', plugin: 'jsdocs', slug: 'coverage', weight: 1 },
66+
{ type: 'audit', plugin: 'coverage', slug: 'coverage', weight: 1 },
67+
],
68+
issues: [],
69+
};
70+
check(ctx);
71+
72+
expect(keyFn).toHaveBeenCalledTimes(3);
73+
expect(errorMsgFn).toHaveBeenCalledWith(['audit coverage/coverage']);
74+
expect(ctx.issues).toEqual([
75+
{
76+
code: 'custom',
77+
message: 'Duplicate refs found: audit coverage/coverage',
78+
input: ctx.value,
79+
},
80+
]);
81+
});
82+
83+
it('add NOT add issue if all keys are unique', () => {
84+
const check = createDuplicatesCheck<CategoryRef>(keyFn, errorMsgFn);
85+
86+
expect(keyFn).not.toHaveBeenCalled();
87+
expect(errorMsgFn).not.toHaveBeenCalled();
88+
89+
const ctx: z.core.ParsePayload<CategoryRef[]> = {
90+
value: [
91+
{ type: 'group', plugin: 'eslint', slug: 'errors', weight: 1 },
92+
{ type: 'group', plugin: 'eslint', slug: 'warnings', weight: 1 },
93+
{ type: 'group', plugin: 'typescript', slug: 'errors', weight: 1 },
94+
],
95+
issues: [],
96+
};
97+
check(ctx);
98+
99+
expect(keyFn).toHaveBeenCalledTimes(3);
100+
expect(errorMsgFn).not.toHaveBeenCalled();
101+
expect(ctx.issues).toEqual([]);
102+
});
103+
});
104+
105+
describe('createDuplicateSlugsCheck', () => {
106+
it('should add issue if there are duplicate slugs', () => {
107+
const check = createDuplicateSlugsCheck<Audit>('Audit');
108+
109+
const ctx: z.core.ParsePayload<Audit[]> = {
110+
value: [
111+
{ slug: 'lcp', title: 'Largest Contentful Paint' },
112+
{ slug: 'cls', title: 'Cumulative Layout Shift' },
113+
{ slug: 'fcp', title: 'First Contentful Paint' },
114+
{ slug: 'lcp', title: 'LCP' },
115+
{ slug: 'fcp', title: 'FCP' },
116+
],
117+
issues: [],
118+
};
119+
check(ctx);
120+
121+
expect(ctx.issues).toEqual([
122+
{
123+
code: 'custom',
124+
message:
125+
'Audit slugs must be unique, but received duplicates: "fcp", "lcp"',
126+
input: ctx.value,
127+
},
128+
]);
129+
});
130+
131+
it('should NOT add issue if all slugs are unique', () => {
132+
const check = createDuplicateSlugsCheck<Audit>('Audit');
133+
134+
const ctx: z.core.ParsePayload<Audit[]> = {
135+
value: [
136+
{ slug: 'lcp', title: 'Largest Contentful Paint' },
137+
{ slug: 'cls', title: 'Cumulative Layout Shift' },
138+
{ slug: 'fcp', title: 'First Contentful Paint' },
139+
],
140+
issues: [],
141+
};
142+
check(ctx);
143+
144+
expect(ctx.issues).toEqual([]);
145+
});
146+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { z } from 'zod/v4';
2+
import {
3+
convertAsyncZodFunctionToSchema,
4+
convertSyncZodFunctionToSchema,
5+
} from './function.js';
6+
7+
describe('convertAsyncZodFunctionToSchema', () => {
8+
it('should create a Zod schema', () => {
9+
expect(
10+
convertAsyncZodFunctionToSchema(
11+
z.function({ output: z.promise(z.url()) }),
12+
),
13+
).toBeInstanceOf(z.ZodType);
14+
});
15+
16+
it('should accept a valid function', async () => {
17+
const schema = convertAsyncZodFunctionToSchema(
18+
z.function({ input: [z.string()], output: z.promise(z.int()) }),
19+
);
20+
21+
const fn = (input: string) => Promise.resolve(input.length);
22+
23+
expect(() => schema.parse(fn)).not.toThrow();
24+
await expect(schema.parse(fn)('')).resolves.toBe(0);
25+
});
26+
27+
it('should reject a non-function value', () => {
28+
const schema = convertAsyncZodFunctionToSchema(
29+
z.function({ input: [z.string()], output: z.promise(z.int()) }),
30+
);
31+
32+
expect(() => schema.parse(123)).toThrow(
33+
'Expected function, received number',
34+
);
35+
});
36+
37+
it('should validate function arguments at runtime', async () => {
38+
const schema = convertAsyncZodFunctionToSchema(
39+
z.function({ input: [z.string()], output: z.promise(z.int()) }),
40+
);
41+
42+
await expect(
43+
schema.parse((input: string) => Promise.resolve(input.length))(
44+
// @ts-expect-error testing invalid argument type
45+
null,
46+
),
47+
).rejects.toThrow('expected string, received null');
48+
});
49+
50+
it('should validate function return type at runtime', async () => {
51+
const schema = convertAsyncZodFunctionToSchema(
52+
z.function({ input: [z.string()], output: z.promise(z.int()) }),
53+
);
54+
55+
await expect(
56+
schema.parse(() => Promise.resolve(Math.random()))(''),
57+
).rejects.toThrow('expected int, received number');
58+
});
59+
60+
it('should add $ZodFunction metadata for zod2md', () => {
61+
const factory = z.function({
62+
input: [z.string()],
63+
output: z.promise(z.int()),
64+
});
65+
const schema = convertAsyncZodFunctionToSchema(factory);
66+
67+
expect(z.globalRegistry.get(schema)).toHaveProperty(
68+
'$ZodFunction',
69+
factory,
70+
);
71+
});
72+
});
73+
74+
describe('convertSyncZodFunctionToSchema', () => {
75+
it('should create a Zod schema', () => {
76+
expect(
77+
convertSyncZodFunctionToSchema(z.function({ output: z.url() })),
78+
).toBeInstanceOf(z.ZodType);
79+
});
80+
81+
it('should accept a valid function', () => {
82+
const schema = convertSyncZodFunctionToSchema(
83+
z.function({ input: [z.string()], output: z.int() }),
84+
);
85+
86+
const fn = (input: string) => input.length;
87+
88+
expect(() => schema.parse(fn)).not.toThrow();
89+
expect(schema.parse(fn)('')).toBe(0);
90+
});
91+
92+
it('should reject a non-function value', () => {
93+
const schema = convertSyncZodFunctionToSchema(
94+
z.function({ input: [z.string()], output: z.int() }),
95+
);
96+
97+
expect(() => schema.parse(123)).toThrow(
98+
'Expected function, received number',
99+
);
100+
});
101+
102+
it('should validate function arguments at runtime', () => {
103+
const schema = convertSyncZodFunctionToSchema(
104+
z.function({ input: [z.string()], output: z.int() }),
105+
);
106+
107+
expect(() =>
108+
schema.parse((input: string) => input.length)(
109+
// @ts-expect-error testing invalid argument type
110+
null,
111+
),
112+
).toThrow('expected string, received null');
113+
});
114+
115+
it('should validate function return type at runtime', () => {
116+
const schema = convertSyncZodFunctionToSchema(
117+
z.function({ input: [z.string()], output: z.int() }),
118+
);
119+
120+
expect(() => schema.parse(() => Math.random())('')).toThrow(
121+
'expected int, received number',
122+
);
123+
});
124+
125+
it('should add $ZodFunction metadata for zod2md', () => {
126+
const factory = z.function({
127+
input: [z.string()],
128+
output: z.int(),
129+
});
130+
const schema = convertSyncZodFunctionToSchema(factory);
131+
132+
expect(z.globalRegistry.get(schema)).toHaveProperty(
133+
'$ZodFunction',
134+
factory,
135+
);
136+
});
137+
});

0 commit comments

Comments
 (0)