Add clipboard provider coverage

active-uix
dev 5 months ago
parent 407eb2f455
commit e22b418324

@ -95,9 +95,11 @@ Actualizacion 2026-05-15:
emitidos por morfo/runtime, estados derivados e indicador sincronizado.
- Cobertura nueva para Collapsible: refs cruzadas trigger/content, eventos
`expand/collapse` y guardas disabled.
- Cobertura nueva para Clipboard: copia via `uix.clipboard`, reset con
`uix.timers`, labels traducidos/override y errores `onError`.
- Validado por fase:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 208 archivos, 2173 tests OK.
- `npm run test` -> 209 archivos, 2175 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.
@ -134,6 +136,7 @@ Actualizacion 2026-05-15:
- `progress-provider.svelte.test.ts` + `meter-provider.svelte.test.ts`
-> 4 tests OK.
- `collapsible-provider.svelte.test.ts` -> 3 tests OK.
- `clipboard-provider.svelte.test.ts` -> 2 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.

@ -527,6 +527,8 @@ Los tests por componente estan creciendo desde las piezas de mayor riesgo. A
emitidos por morfo/runtime, estados derivados e indicador sincronizado.
- `collapsible/collapsible-provider.svelte.test.ts` — refs cruzadas
trigger/content, eventos `expand/collapse` y guardas disabled.
- `clipboard/clipboard-provider.svelte.test.ts` — copia via `uix.clipboard`,
reset con `uix.timers`, labels traducidos/override y errores `onError`.
Pendiente: seguir ampliando cobertura por el resto del catalogo Soma, ya por
componentes de riesgo medio y familias menos centrales. `table-core`,

@ -0,0 +1,188 @@
// @vitest-environment jsdom
import { tick } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
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 {
ClipboardIndicatorProvider,
ClipboardProvider,
ClipboardTriggerProvider
} from './clipboard-provider.svelte';
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function installSomaHarness(writeText = vi.fn<() => Promise<void>>().mockResolvedValue(undefined)) {
const dom = createActiveDom();
const timerCancel = vi.fn();
const timerReschedule = vi.fn();
let scheduledTask: (() => void) | undefined;
const schedule = vi.fn((_key: string, _delay: number, task: () => void) => {
scheduledTask = task;
return {
cancel: timerCancel,
reschedule: timerReschedule
};
});
const soma = {
dom,
langs: {
ts: vi.fn((key: string) => (key.includes('copied') ? 'Copied' : 'Copy to clipboard'))
},
uix: {
clipboard: { writeText },
timers: { schedule }
},
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(ClipboardProvider.ctx, 'set').mockImplementation((value) => value);
return {
dom,
writeText,
schedule,
timerCancel,
get scheduledTask() {
return scheduledTask;
}
};
}
function clipboardOpts(root = document.createElement('div')) {
return {
id: state('clipboard-root'),
ref: state<HTMLElement | null>(root),
value: state('copy me'),
timeout: state(1500),
onCopy: state<((value: string) => void) | undefined>(undefined),
onError: state<((err: unknown) => void) | undefined>(undefined)
};
}
describe('ClipboardProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('copies value, projects copied state and resets through UIX timers', async () => {
const harness = installSomaHarness();
const root = document.createElement('div');
const triggerEl = document.createElement('button');
const indicatorEl = document.createElement('span');
document.body.append(root, triggerEl, indicatorEl);
const opts = clipboardOpts(root);
const onCopy = vi.fn();
opts.onCopy.current = onCopy;
const { result, cleanup } = withEffectRoot(() => {
const provider = ClipboardProvider.create(opts);
vi.spyOn(ClipboardProvider, 'require').mockReturnValue(provider);
const trigger = ClipboardTriggerProvider.create({
id: state('clipboard-trigger'),
ref: state<HTMLElement | null>(triggerEl),
ariaLabel: state<string | undefined>(undefined)
});
const indicator = ClipboardIndicatorProvider.create({
id: state('clipboard-indicator'),
ref: state<HTMLElement | null>(indicatorEl)
});
return { provider, trigger, indicator };
});
expect(result.provider.snippetProps).toMatchObject({
value: 'copy me',
copied: false
});
expect(result.indicator.isPresent).toBe(false);
await tick();
expect(triggerEl.getAttribute('aria-label')).toBe('Copy to clipboard');
expect(root.hasAttribute('data-copied')).toBe(false);
await result.provider.copy();
expect(harness.writeText).toHaveBeenCalledWith('copy me');
expect(onCopy).toHaveBeenCalledWith('copy me');
expect(result.provider.copied).toBe(true);
expect(result.indicator.isPresent).toBe(true);
expect(harness.schedule).toHaveBeenCalledWith(
'soma:clipboard:clipboard-root:reset',
1500,
expect.any(Function),
expect.objectContaining({
replace: true,
meta: { component: 'clipboard', action: 'reset-copied' }
})
);
await tick();
expect(root.getAttribute('data-copied')).toBe('');
expect(triggerEl.getAttribute('data-copied')).toBe('');
expect(triggerEl.getAttribute('aria-label')).toBe('Copied');
expect(indicatorEl.getAttribute('data-copied')).toBe('');
harness.scheduledTask?.();
await tick();
expect(result.provider.copied).toBe(false);
expect(root.hasAttribute('data-copied')).toBe(false);
expect(triggerEl.getAttribute('aria-label')).toBe('Copy to clipboard');
cleanup();
harness.dom.dispose();
});
it('uses trigger label overrides and reports clipboard errors', async () => {
const error = new Error('denied');
const harness = installSomaHarness(vi.fn<() => Promise<void>>().mockRejectedValue(error));
const root = document.createElement('div');
const triggerEl = document.createElement('button');
document.body.append(root, triggerEl);
const opts = clipboardOpts(root);
const onError = vi.fn();
opts.onError.current = onError;
const { result, cleanup } = withEffectRoot(() => {
const provider = ClipboardProvider.create(opts);
vi.spyOn(ClipboardProvider, 'require').mockReturnValue(provider);
const trigger = ClipboardTriggerProvider.create({
id: state('clipboard-trigger'),
ref: state<HTMLElement | null>(triggerEl),
ariaLabel: state<string | undefined>('Copy code')
});
return { provider, trigger };
});
await tick();
expect(triggerEl.getAttribute('aria-label')).toBe('Copy code');
await expect(result.provider.copy()).rejects.toThrow('denied');
expect(onError).toHaveBeenCalledWith(error);
expect(result.provider.copied).toBe(false);
expect(harness.schedule).not.toHaveBeenCalled();
result.trigger.onclick({ currentTarget: triggerEl } as never);
await Promise.resolve();
expect(onError).toHaveBeenCalledTimes(2);
cleanup();
harness.dom.dispose();
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.