fix(@vben-core/form-ui): harden codec value boundaries
This commit is contained in:
@@ -3,6 +3,7 @@ import type { BaseFormComponentType } from '../src/types';
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { FormApi } from '../src/form-api';
|
import { FormApi } from '../src/form-api';
|
||||||
|
import { FormCodecError } from '../src/form-codec';
|
||||||
|
|
||||||
describe('formApi', () => {
|
describe('formApi', () => {
|
||||||
let formApi: FormApi;
|
let formApi: FormApi;
|
||||||
@@ -171,6 +172,71 @@ describe('formApi', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should isolate codec results from live form values', async () => {
|
||||||
|
interface ProfileFormValues {
|
||||||
|
profile: { name: string };
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const values: ProfileFormValues = {
|
||||||
|
profile: { name: 'Ada' },
|
||||||
|
tags: ['admin'],
|
||||||
|
};
|
||||||
|
const codecFormApi = new FormApi<ProfileFormValues>({
|
||||||
|
codec: {
|
||||||
|
decode: (submitValues) => submitValues,
|
||||||
|
encode: (formValues) => ({
|
||||||
|
profile: formValues.profile,
|
||||||
|
tags: formValues.tags,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const formActions: any = { meta: {}, values };
|
||||||
|
|
||||||
|
codecFormApi.mount(formActions, new Map());
|
||||||
|
const initialSubmissionValues = codecFormApi.getLatestSubmissionValues();
|
||||||
|
values.profile.name = 'Grace';
|
||||||
|
values.tags.push('user');
|
||||||
|
|
||||||
|
expect(initialSubmissionValues).toEqual({
|
||||||
|
profile: { name: 'Ada' },
|
||||||
|
tags: ['admin'],
|
||||||
|
});
|
||||||
|
const submissionValues = await codecFormApi.getValues();
|
||||||
|
expect(submissionValues.profile).not.toBe(values.profile);
|
||||||
|
expect(submissionValues.tags).not.toBe(values.tags);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to raw values when the initial codec encode fails', async () => {
|
||||||
|
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
const codecFormApi = new FormApi<
|
||||||
|
{ name?: string },
|
||||||
|
BaseFormComponentType,
|
||||||
|
Record<never, never>,
|
||||||
|
{ normalizedName: string }
|
||||||
|
>({
|
||||||
|
codec: {
|
||||||
|
decode: (values) => ({ name: values.normalizedName }),
|
||||||
|
encode() {
|
||||||
|
throw new Error('incomplete initial values');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const formActions: any = { meta: {}, values: { name: 'Ada' } };
|
||||||
|
|
||||||
|
expect(() => codecFormApi.mount(formActions, new Map())).not.toThrow();
|
||||||
|
|
||||||
|
expect(codecFormApi.isMounted).toBe(true);
|
||||||
|
expect(codecFormApi.getLatestSubmissionValues()).toEqual({ name: 'Ada' });
|
||||||
|
expect(warning).toHaveBeenCalledWith(
|
||||||
|
'[Vben Form] Failed to encode initial values. Falling back to raw form values.',
|
||||||
|
expect.objectContaining({ phase: 'encode' }),
|
||||||
|
);
|
||||||
|
await expect(codecFormApi.getValues()).rejects.toBeInstanceOf(
|
||||||
|
FormCodecError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should scan deprecated schema transforms once for unchanged state', async () => {
|
it('should scan deprecated schema transforms once for unchanged state', async () => {
|
||||||
const getChildren = vi.fn(() => []);
|
const getChildren = vi.fn(() => []);
|
||||||
const schema = {
|
const schema = {
|
||||||
|
|||||||
@@ -228,6 +228,10 @@ describe('form public types', () => {
|
|||||||
handleReset(values) {
|
handleReset(values) {
|
||||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||||
},
|
},
|
||||||
|
handleSubmit(values, rawValues) {
|
||||||
|
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||||
|
expectTypeOf(rawValues).toEqualTypeOf<Readonly<AccountFormValues>>();
|
||||||
|
},
|
||||||
schema: [],
|
schema: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export class FormApi<
|
|||||||
if (this.state?.codec) {
|
if (this.state?.codec) {
|
||||||
return encodeFormValues(
|
return encodeFormValues(
|
||||||
this.state.codec,
|
this.state.codec,
|
||||||
rawValues as Readonly<TFormValues>,
|
cloneDeep(toRaw(rawValues)) as Readonly<TFormValues>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return formatFormValues(
|
return formatFormValues(
|
||||||
@@ -303,10 +303,23 @@ export class FormApi<
|
|||||||
if (!this.isMounted) {
|
if (!this.isMounted) {
|
||||||
this.form = formActions;
|
this.form = formActions;
|
||||||
this.stateHandler.setConditionTrue();
|
this.stateHandler.setConditionTrue();
|
||||||
const initialValues = this.form.values
|
let initialValues: FormValues = {};
|
||||||
? this.formatValues(toRaw(this.form.values))
|
if (this.form.values) {
|
||||||
: {};
|
const rawInitialValues = toRaw(this.form.values);
|
||||||
this.setLatestSubmissionValues(initialValues);
|
try {
|
||||||
|
initialValues = this.formatValues(rawInitialValues);
|
||||||
|
} catch (error) {
|
||||||
|
if (!this.state?.codec) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
'[Vben Form] Failed to encode initial values. Falling back to raw form values.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
initialValues = cloneDeep(rawInitialValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setLatestSubmissionValues(initialValues as Partial<TSubmitValues>);
|
||||||
this.componentRefMap =
|
this.componentRefMap =
|
||||||
componentRefMap ?? this.componentRefMap ?? new Map();
|
componentRefMap ?? this.componentRefMap ?? new Map();
|
||||||
this.isMounted = true;
|
this.isMounted = true;
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export type HandleSubmitFn<
|
|||||||
TFormValues extends FormValues = FormValues,
|
TFormValues extends FormValues = FormValues,
|
||||||
TSubmitValues extends FormValues = TFormValues,
|
TSubmitValues extends FormValues = TFormValues,
|
||||||
> = (
|
> = (
|
||||||
values: TSubmitValues,
|
values: NoInfer<TSubmitValues>,
|
||||||
rawValues: Readonly<TFormValues>,
|
rawValues: Readonly<TFormValues>,
|
||||||
) => Promise<void> | void;
|
) => Promise<void> | void;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user