Add date field provider coverage

active-uix
dev 5 months ago
parent 2b36e2036c
commit 5878e780bc

@ -38,9 +38,13 @@ Actualizacion 2026-05-15:
`areAllTimeSegmentsFilledDOM(...)` del helper de fecha.
- Cobertura nueva para DateRangeField: validación de rango y foco de label
via `ActiveDom`.
- Cobertura ampliada para DateFieldProvider: validación, sync de segmentos
date/time y commit solo cuando los segmentos renderizados estan completos.
- Correccion DateField: el announcer se inicializa con `$effect` en vez de
`onMount()` dentro de la clase.
- Validado por fase:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 184 archivos, 2077 tests OK.
- `npm run test` -> 184 archivos, 2080 tests OK.
- tests focales de Select/Popover/Toast/RangeCalendar -> 11 tests OK.
- tests focales Dialog/Drawer/Popover/Select -> 10 tests OK.
- `command-provider.svelte.test.ts` -> 2 tests OK.
@ -52,6 +56,7 @@ Actualizacion 2026-05-15:
- `time-range-field-provider.svelte.test.ts` -> 3 tests OK.
- `time-field-provider.svelte.test.ts` -> 3 tests OK.
- `date-range-field-provider.svelte.test.ts` -> 3 tests OK.
- `date-field-provider.svelte.test.ts` -> 7 tests OK.
- guardia `rg "from './.*provider\\.svelte'" src/uix/soma/components -g 'exports.ts'`
-> solo queda `select/components/select-provider.svelte`, que es wrapper
publico, no runtime provider.

@ -462,7 +462,7 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
- `range-calendar/range-calendar-provider.svelte.test.ts` — placeholder,
orden de endpoints y limites min/max.
- `date-field/date-field-provider.svelte.test.ts` — helpers UI de segmentos,
lectura DOM y navegación por `ActiveDom`.
lectura DOM, navegación por `ActiveDom`, validación y commit segmentado.
- `date-picker/date-picker-provider.svelte.test.ts` — registro runtime y
cierre controlado por `closeOnDateSelect`.
- `date-range-picker/date-range-picker-provider.svelte.test.ts` — registro

@ -1,9 +1,21 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
import { CalendarDate } from '$libs/days';
import { CalendarDate, CalendarDateTime } from '$libs/days';
import type {
DateOnInvalid,
DateValidator,
DateValue,
EditableSegmentPart,
Granularity,
HourCycle
} from '$libs/days';
import { state } from '$libs/reactive';
import type { Morfo } from '$uix/morfo';
import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import {
areAllSegmentsFilledDOM,
getFirstSegment,
@ -12,6 +24,9 @@ import {
initSegmentStates,
isAcceptableSegmentKey
} from '$soma/datetime';
import { FieldProvider } from '../field/field-provider.svelte';
import { DateFieldProvider } from './date-field-provider.svelte';
function createField() {
const field = document.createElement('div');
@ -31,7 +46,75 @@ function createField() {
return field;
}
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function installSomaHarness(hourCycle?: HourCycle) {
const dom = createActiveDom();
const soma = {
dom,
langs: {
getLocale: () => 'en-US',
ts: (key: string) => key
},
dates: {
getDateOrder: () => 'MDY',
getHourCycle: () => hourCycle
},
prefs: {
getDir: () => 'ltr'
},
logger: {
warn: vi.fn()
},
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(FieldProvider, 'get').mockReturnValue(undefined);
vi.spyOn(DateFieldProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function createDateFieldOpts() {
return {
id: state('date-field-root'),
ref: state<HTMLElement | null>(null),
value: state<DateValue | undefined>(undefined),
placeholder: state<DateValue>(new CalendarDate(2026, 1, 1)),
readonlySegments: state<EditableSegmentPart[]>([]),
validate: state<DateValidator | undefined>(undefined),
onInvalid: state<DateOnInvalid | undefined>(undefined),
minValue: state<DateValue | undefined>(undefined),
maxValue: state<DateValue | undefined>(undefined),
disabled: state(false),
readonly: state(false),
granularity: state<Granularity | undefined>(undefined),
hourCycle: state<HourCycle | undefined>(undefined),
locale: state('en-US'),
hideTimeZone: state(false),
required: state(false),
errorMessageId: state<string | undefined>(undefined)
};
}
describe('DateField segment helpers', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('creates fresh segment state maps for each field instance', () => {
const first = initSegmentStates();
const second = initSegmentStates();
@ -79,3 +162,78 @@ describe('DateField segment helpers', () => {
dom.dispose();
});
});
describe('DateFieldProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('validates custom, min and max constraints for the current value', () => {
const { dom } = installSomaHarness();
const opts = createDateFieldOpts();
opts.value.current = new CalendarDate(2026, 4, 30);
opts.minValue.current = new CalendarDate(2026, 5, 1);
const { result: provider, cleanup } = withEffectRoot(() => DateFieldProvider.create(opts));
expect(provider.validationStatus).toEqual({ reason: 'min' });
opts.minValue.current = undefined;
opts.maxValue.current = new CalendarDate(2026, 5, 31);
opts.value.current = new CalendarDate(2026, 6, 1);
expect(provider.validationStatus).toEqual({ reason: 'max' });
opts.maxValue.current = undefined;
opts.validate.current = () => 'Blocked date';
expect(provider.validationStatus).toEqual({
reason: 'custom',
message: 'Blocked date'
});
cleanup();
dom.dispose();
});
it('does not commit a value until all rendered date segments are filled', () => {
const { dom } = installSomaHarness();
const opts = createDateFieldOpts();
const { result: provider, cleanup } = withEffectRoot(() => DateFieldProvider.create(opts));
provider.setFieldNode(createField());
provider.updateSegment('month', () => '05');
expect(opts.value.current).toBeUndefined();
provider.updateSegment('day', () => '15');
expect(opts.value.current).toBeUndefined();
provider.updateSegment('year', () => '2026');
expect(opts.value.current?.year).toBe(2026);
expect(opts.value.current?.month).toBe(5);
expect(opts.value.current?.day).toBe(15);
cleanup();
dom.dispose();
});
it('syncs padded date and time segments from a concrete value', () => {
const { dom } = installSomaHarness(12);
const opts = createDateFieldOpts();
opts.placeholder.current = new CalendarDateTime(2026, 5, 15, 13, 5, 10);
opts.value.current = new CalendarDateTime(2026, 5, 15, 13, 5, 10);
const { result: provider, cleanup } = withEffectRoot(() => DateFieldProvider.create(opts));
expect(provider.segmentValues.month).toBe('05');
expect(provider.segmentValues.day).toBe('15');
expect(provider.segmentValues.year).toBe('2026');
expect((provider.segmentValues as { hour: string }).hour).toBe('13');
expect((provider.segmentValues as { minute: string }).minute).toBe('05');
expect((provider.segmentValues as { second: string }).second).toBe('10');
expect((provider.segmentValues as { dayPeriod: 'AM' | 'PM' }).dayPeriod).toBe('PM');
cleanup();
dom.dispose();
});
});

@ -1,5 +1,5 @@
import type { Updater } from 'svelte/store';
import { untrack, onMount } from 'svelte';
import { untrack } from 'svelte';
import { watch } from 'runed';
import { context, type ProviderOpts, type WithRefOpts } from '../../provider';
import { attachRef, type RefAttachment } from '$libs/reactive';
@ -243,7 +243,7 @@ export class DateFieldProvider {
this.segmentValues = initializeSegmentValues(this.inferredGranularity);
onMount(() => {
$effect(() => {
this.announcer = getAnnouncer(this.soma.dom, opts.ref?.current);
});

Loading…
Cancel
Save

Powered by TurnKey Linux.