parent
0464111330
commit
ccded8bc4a
@ -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…
Reference in new issue