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 { FormApi } from '../src/form-api';
|
||||
import { FormCodecError } from '../src/form-codec';
|
||||
|
||||
describe('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 () => {
|
||||
const getChildren = vi.fn(() => []);
|
||||
const schema = {
|
||||
|
||||
@@ -228,6 +228,10 @@ describe('form public types', () => {
|
||||
handleReset(values) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
},
|
||||
handleSubmit(values, rawValues) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
expectTypeOf(rawValues).toEqualTypeOf<Readonly<AccountFormValues>>();
|
||||
},
|
||||
schema: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export class FormApi<
|
||||
if (this.state?.codec) {
|
||||
return encodeFormValues(
|
||||
this.state.codec,
|
||||
rawValues as Readonly<TFormValues>,
|
||||
cloneDeep(toRaw(rawValues)) as Readonly<TFormValues>,
|
||||
);
|
||||
}
|
||||
return formatFormValues(
|
||||
@@ -303,10 +303,23 @@ export class FormApi<
|
||||
if (!this.isMounted) {
|
||||
this.form = formActions;
|
||||
this.stateHandler.setConditionTrue();
|
||||
const initialValues = this.form.values
|
||||
? this.formatValues(toRaw(this.form.values))
|
||||
: {};
|
||||
this.setLatestSubmissionValues(initialValues);
|
||||
let initialValues: FormValues = {};
|
||||
if (this.form.values) {
|
||||
const rawInitialValues = toRaw(this.form.values);
|
||||
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 =
|
||||
componentRefMap ?? this.componentRefMap ?? new Map();
|
||||
this.isMounted = true;
|
||||
|
||||
@@ -678,7 +678,7 @@ export type HandleSubmitFn<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> = (
|
||||
values: TSubmitValues,
|
||||
values: NoInfer<TSubmitValues>,
|
||||
rawValues: Readonly<TFormValues>,
|
||||
) => Promise<void> | void;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user