Add color field provider coverage

active-uix
dev 5 months ago
parent cab1363616
commit 813c4c440c

@ -55,9 +55,11 @@ Actualizacion 2026-05-15:
lugar del prop crudo.
- Cobertura nueva para FileUpload: aceptación/rechazo de ficheros, dropzone,
input oculto, items, progress, remove y clear.
- Cobertura nueva para ColorField: commit por segmentos, edición hex con
teclado, selector de formato, input oculto y foco de label via `ActiveDom`.
- Validado por fase:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 188 archivos, 2093 tests OK.
- `npm run test` -> 189 archivos, 2097 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.
@ -74,6 +76,7 @@ Actualizacion 2026-05-15:
- `virtual-grid-provider.svelte.test.ts` -> 2 tests OK.
- `number-field-provider.svelte.test.ts` -> 4 tests OK.
- `file-upload-provider.svelte.test.ts` -> 4 tests OK.
- `color-field-provider.svelte.test.ts` -> 4 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.

@ -486,6 +486,9 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
teclado spinbutton, triggers, props ARIA y scrubber.
- `file-upload/file-upload-provider.svelte.test.ts` — aceptación/rechazo,
dropzone, input oculto, items, progress y acciones remove/clear.
- `color-field/color-field-provider.svelte.test.ts` — commit por segmentos,
edición hex por teclado, selector de formato, input oculto y foco via
`ActiveDom`.
Pendiente: seguir ampliando cobertura por familias de alto riesgo fuera del
bloque fecha/hora, virtualización, inputs numericos y file upload.

@ -0,0 +1,258 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest';
import { tick } from 'svelte';
import { createActiveDom } from '$adom';
import {
colorValueFromRgb,
DEFAULT_COLOR,
type ColorFormat,
type ColorValue,
type EditableColorSegmentPart
} from '$libs/color';
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 { FieldProvider } from '../field/field-provider.svelte';
import {
ColorFieldFormatSelectProvider,
ColorFieldHiddenInputProvider,
ColorFieldInputProvider,
ColorFieldLabelProvider,
ColorFieldProvider,
createColorSegmentProvider
} from './color-field-provider.svelte';
import type { ColorOnInvalid, ColorValidator } from './types';
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function installSomaHarness() {
const dom = createActiveDom();
const soma = {
dom,
langs: {
ts: (key: string) => key
},
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(ColorFieldProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function createColorFieldOpts() {
return {
id: state('color-field-root'),
ref: state<HTMLElement | null>(null),
value: state<ColorValue | undefined>(undefined),
placeholder: state<ColorValue>(DEFAULT_COLOR),
format: state<ColorFormat>('rgb'),
allowedFormats: state<ColorFormat[]>(['hex', 'rgb', 'hsl']),
enableAlpha: state(false),
validate: state<ColorValidator | undefined>(undefined),
onInvalid: state<ColorOnInvalid | undefined>(undefined),
disabled: state(false),
readonly: state(false),
readonlySegments: state<EditableColorSegmentPart[]>([]),
required: state(false),
invalid: state(false),
errorMessageId: state<string | undefined>(undefined),
dir: state<'ltr' | 'rtl'>('ltr')
};
}
function keyEvent(key: string, target: HTMLElement = document.createElement('span')) {
return {
key,
ctrlKey: false,
metaKey: false,
currentTarget: target,
preventDefault: vi.fn()
};
}
describe('ColorFieldProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('commits an RGB value only when the required editable segments are filled', () => {
const { dom } = installSomaHarness();
const opts = createColorFieldOpts();
const { result: provider, cleanup } = withEffectRoot(() => ColorFieldProvider.create(opts));
provider.updateSegment('r', () => '10');
provider.updateSegment('g', () => '20');
expect(opts.value.current).toBeUndefined();
provider.updateSegment('b', () => '30');
expect(opts.value.current?.rgb).toEqual({ r: 10, g: 20, b: 30, a: 1 });
expect(opts.value.current?.hex).toBe('#0a141e');
expect(provider.formattedValue).toBe('rgb(10, 20, 30)');
expect(provider.props).toMatchObject({
dir: 'ltr',
'data-format': 'rgb',
'data-invalid': undefined
});
cleanup();
dom.dispose();
});
it('edits a hex segment with keyboard input and exposes spinbutton aria', () => {
const { dom } = installSomaHarness();
const opts = createColorFieldOpts();
opts.format.current = 'hex';
const { result: provider, cleanup } = withEffectRoot(() => ColorFieldProvider.create(opts));
vi.spyOn(ColorFieldProvider.ctx, 'get').mockReturnValue(provider);
const target = document.createElement('span');
const segment = createColorSegmentProvider('hex', {
id: state('color-field-hex'),
ref: state<HTMLElement | null>(target)
});
for (const key of ['f', 'f', '6', '6', '0', '0']) {
(segment.props as { onkeydown: (event: unknown) => void }).onkeydown(keyEvent(key, target));
}
expect(opts.value.current?.hex).toBe('#ff6600');
expect(segment.props).toMatchObject({
role: 'spinbutton',
'data-segment': 'hex',
'data-placeholder': undefined,
'aria-valuemin': 0,
'aria-valuemax': 16777215,
'aria-valuetext': '#ff6600'
});
cleanup();
dom.dispose();
});
it('switches format only within allowed formats and keeps select props locked when needed', () => {
const { dom } = installSomaHarness();
const opts = createColorFieldOpts();
opts.value.current = colorValueFromRgb({ r: 255, g: 128, b: 0, a: 1 });
opts.allowedFormats.current = ['hex', 'hsl'];
const { result: provider, cleanup } = withEffectRoot(() => ColorFieldProvider.create(opts));
vi.spyOn(ColorFieldProvider.ctx, 'get').mockReturnValue(provider);
const select = ColorFieldFormatSelectProvider.create({
id: state('color-field-format'),
ref: state<HTMLElement | null>(null),
ariaLabel: state<string | undefined>(undefined)
});
select.onchange({ target: { value: 'hsl' } } as never);
expect(opts.format.current).toBe('hsl');
expect(provider.segmentValues.h).toBe('30');
expect(provider.segmentValues.s).toBe('100');
expect(provider.segmentValues.l).toBe('50');
expect(select.options).toEqual(['hex', 'hsl']);
expect(select.props['data-locked']).toBeUndefined();
opts.allowedFormats.current = ['hsl'];
expect(select.isLocked).toBe(true);
expect(select.props['data-locked']).toBe('');
provider.setFormat('rgb');
expect(opts.format.current).toBe('hsl');
cleanup();
dom.dispose();
});
it('wires input, label focus and hidden form value through ActiveDom', async () => {
const { dom } = installSomaHarness();
const opts = createColorFieldOpts();
opts.value.current = colorValueFromRgb({ r: 1, g: 2, b: 3, a: 1 });
opts.required.current = true;
const focus = vi.spyOn(dom, 'focus');
const field = document.createElement('div');
const firstSegment = document.createElement('span');
firstSegment.dataset.segment = 'r';
field.appendChild(firstSegment);
const labelNode = document.createElement('label');
labelNode.id = 'color-label';
const { result, cleanup } = withEffectRoot(() => {
const provider = ColorFieldProvider.create(opts);
vi.spyOn(ColorFieldProvider.ctx, 'get').mockReturnValue(provider);
provider.setFieldNode(field);
const label = ColorFieldLabelProvider.create({
id: state('color-label'),
ref: state<HTMLElement | null>(labelNode)
});
provider.setLabelNode(labelNode);
const input = ColorFieldInputProvider.create({
id: state('color-field-input'),
ref: state<HTMLElement | null>(field),
name: state('brandColor'),
ariaLabel: state<string | undefined>(undefined),
ariaLabelledBy: state<string | undefined>(undefined)
});
const hidden = ColorFieldHiddenInputProvider.create({
id: state('color-field-hidden')
});
return { provider, label, input, hidden };
});
await tick();
expect(result.input.segments.map((segment) => segment.part)).toEqual([
'r',
'literal',
'g',
'literal',
'b'
]);
expect(result.input.props).toMatchObject({
role: 'group',
'aria-labelledby': 'color-label',
'aria-disabled': 'false',
'data-format': 'rgb'
});
expect(result.hidden.props).toMatchObject({
type: 'hidden',
name: 'brandColor',
value: 'rgb(1, 2, 3)',
required: true,
'aria-hidden': 'true'
});
result.label.onclick();
expect(focus).toHaveBeenCalledWith(firstSegment);
cleanup();
dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.