Add form provider coverage

active-uix
dev 5 months ago
parent 0464111330
commit ccded8bc4a

@ -139,9 +139,11 @@ Actualizacion 2026-05-15:
touch guard, content hover/cancelClose y props Floating/Arrow. touch guard, content hover/cancelClose y props Floating/Arrow.
- Cobertura nueva para Table: root/section/header/cell props desde - Cobertura nueva para Table: root/section/header/cell props desde
`$libs/datagrid`, selección de fila y disclosure `RowDetail`. `$libs/datagrid`, selección de fila y disclosure `RowDetail`.
- Cobertura nueva para Form: submit inválido con foco via `ActiveDom`,
attrs runtime y partes Submit/Reset/ErrorSummary sobre `$libs/forms`.
- Validado por fase: - Validado por fase:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 230 archivos, 2236 tests OK. - `npm run test` -> 231 archivos, 2238 tests OK.
- tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK. - tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK.
- tests focales Dialog/Drawer/Popover/Select -> 10 tests OK. - tests focales Dialog/Drawer/Popover/Select -> 10 tests OK.
- `command-provider.svelte.test.ts` -> 2 tests OK. - `command-provider.svelte.test.ts` -> 2 tests OK.
@ -200,6 +202,7 @@ Actualizacion 2026-05-15:
- `grid-list-provider.svelte.test.ts` -> 3 tests OK. - `grid-list-provider.svelte.test.ts` -> 3 tests OK.
- `link-preview-provider.svelte.test.ts` -> 2 tests OK. - `link-preview-provider.svelte.test.ts` -> 2 tests OK.
- `table-provider.svelte.test.ts` -> 2 tests OK. - `table-provider.svelte.test.ts` -> 2 tests OK.
- `form-provider.svelte.test.ts` -> 2 tests OK.
- guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'` - guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'`
-> solo queda `select/components/select-provider.svelte`, que es wrapper -> solo queda `select/components/select-provider.svelte`, que es wrapper
publico, no runtime provider. publico, no runtime provider.

@ -571,6 +571,8 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
con `uix.timers`, touch guard, content hover y props Floating/Arrow. con `uix.timers`, touch guard, content hover y props Floating/Arrow.
- `table/table-provider.svelte.test.ts` — root/section/header/cell props - `table/table-provider.svelte.test.ts` — root/section/header/cell props
desde `$libs/datagrid`, selección de fila y disclosure `RowDetail`. desde `$libs/datagrid`, selección de fila y disclosure `RowDetail`.
- `form/form-provider.svelte.test.ts` — submit inválido con foco via
`ActiveDom`, attrs runtime y partes Submit/Reset/ErrorSummary.
Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por
componentes de riesgo medio y familias menos centrales. `table-core`, componentes de riesgo medio y familias menos centrales. `table-core`,

@ -0,0 +1,221 @@
// @vitest-environment jsdom
import { tick } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
import { createForm, type Form } from '$libs/forms';
import { state } from '$libs/reactive';
import type { StandardSchemaV1 } from '$libs/standard-schema';
import type { Morfo } from '$uix/morfo';
import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import {
FormErrorSummaryProvider,
FormProvider,
FormResetProvider,
FormSubmitProvider
} from './form-provider.svelte';
type LoginValues = {
email: string;
};
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function loginSchema(): StandardSchemaV1<LoginValues> {
return {
'~standard': {
version: 1,
vendor: 'test',
validate(value) {
const values = value as LoginValues;
if (values.email.includes('@')) return { value: values };
return {
issues: [
{
message: 'Invalid email',
path: ['email']
}
]
};
}
}
};
}
function createLoginForm() {
const form = createForm({
defaults: { email: '' },
schema: loginSchema()
});
form.registerField('email', {});
return form;
}
function installSomaHarness() {
const dom = createActiveDom();
const soma = {
dom,
langs: {
ts: vi.fn((key: string) => (key.includes('reset') ? 'Reset' : 'Submit'))
},
runtime: (morfo: Morfo, sources: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, {
dom,
translate: (key) => key,
...sources
})
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(FormProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function formOpts(form: Form<LoginValues>, root: HTMLFormElement) {
return {
id: state('login-form'),
ref: state<HTMLElement | null>(root),
form: state(form),
schema: state<StandardSchemaV1 | undefined>(loginSchema()),
noValidate: state(true),
focusFirstError: state(true)
};
}
function submitEvent() {
return {
preventDefault: vi.fn()
};
}
describe('FormProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('submits through the form handle and focuses the first invalid field', async () => {
const { dom } = installSomaHarness();
const focusSpy = vi.spyOn(dom, 'focus');
const root = document.createElement('form');
const field = document.createElement('div');
const input = document.createElement('input');
field.setAttribute('data-field', '');
field.setAttribute('data-field-name', 'email');
field.appendChild(input);
root.appendChild(field);
document.body.appendChild(root);
const form = createLoginForm();
const opts = formOpts(form, root);
const { result: provider, cleanup } = withEffectRoot(() => FormProvider.create(opts));
expect(provider.props).toMatchObject({
id: 'login-form',
'data-form': '',
novalidate: true,
onsubmit: provider.handleSubmit
});
expect(provider.getFieldElementId('email')).toBe('login-form-field-email');
expect(provider.snippetProps).toMatchObject({
isPending: false,
isValid: true,
isDirty: false,
isTouched: false,
submitCount: 0
});
const event = submitEvent();
await provider.handleSubmit(event as unknown as SubmitEvent);
await tick();
expect(event.preventDefault).toHaveBeenCalledOnce();
expect(form.submitCount).toBe(1);
expect(provider.isSubmitted).toBe(true);
expect(provider.isInvalid).toBe(true);
expect(focusSpy).toHaveBeenCalledWith(input);
expect(root.getAttribute('data-invalid')).toBe('');
expect(root.getAttribute('data-submitted')).toBe('');
cleanup();
dom.dispose();
});
it('projects submit/reset/error-summary state from the form', async () => {
const { dom } = installSomaHarness();
const root = document.createElement('form');
document.body.appendChild(root);
const form = createLoginForm();
form.values.email = 'bad';
form.validate();
const opts = formOpts(form, root);
opts.focusFirstError.current = false;
const { result, cleanup } = withEffectRoot(() => {
const provider = FormProvider.create(opts);
vi.spyOn(FormProvider, 'require').mockReturnValue(provider);
const submit = FormSubmitProvider.create({
id: state('form-submit'),
ref: state<HTMLElement | null>(document.createElement('button')),
ariaLabel: state<string | undefined>(undefined)
});
const reset = FormResetProvider.create({
id: state('form-reset'),
ref: state<HTMLElement | null>(document.createElement('button')),
ariaLabel: state<string | undefined>('Clear form'),
disableWhenPristine: state(true)
});
const summary = FormErrorSummaryProvider.create({
id: state('form-summary'),
ref: state<HTMLElement | null>(document.createElement('div')),
hideUntilSubmitted: state(true)
});
return { provider, submit, reset, summary };
});
expect(result.submit.props).toMatchObject({
type: 'submit',
'aria-label': 'Submit',
'aria-busy': 'false',
disabled: undefined
});
expect(result.reset.props).toMatchObject({
type: 'button',
'aria-label': 'Clear form',
disabled: undefined,
'data-dirty': ''
});
expect(result.summary.count).toBe(1);
expect(result.summary.fieldErrors).toEqual([{ field: 'email', message: 'Invalid email' }]);
expect(result.summary.props).toMatchObject({
role: 'alert',
'aria-live': 'polite',
hidden: true,
'data-hidden': ''
});
await result.provider.handleSubmit(submitEvent() as unknown as SubmitEvent);
await tick();
expect(result.summary.props.hidden).toBeUndefined();
expect(result.summary.props['data-hidden']).toBeUndefined();
result.reset.onclick();
await tick();
expect(form.values.email).toBe('');
expect(result.reset.props.disabled).toBe(true);
expect(result.summary.count).toBe(0);
expect(result.summary.props.hidden).toBe(true);
cleanup();
dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.