Files
tools/vue/primitives/src/utilities/visually-hidden/__test__/VisuallyHiddenInput.test.ts
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
2026-06-15 16:54:29 +07:00

227 lines
7.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VisuallyHiddenInput from '../VisuallyHiddenInput.vue';
import VisuallyHiddenInputBubble from '../VisuallyHiddenInputBubble.vue';
function inputs(el: Element): HTMLInputElement[] {
return Array.from(el.querySelectorAll('input')) as HTMLInputElement[];
}
describe('VisuallyHiddenInput', () => {
let wrapper: ReturnType<typeof mount> | undefined;
afterEach(() => {
wrapper?.unmount();
wrapper = undefined;
});
it('renders a single hidden input for a primitive value', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'color', value: 'red' },
});
const all = inputs(wrapper.element.parentElement!);
expect(all).toHaveLength(1);
expect(all[0]!.name).toBe('color');
expect(all[0]!.value).toBe('red');
});
it('hides the input from layout and the accessibility tree by default', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'color', value: 'red' },
});
const input = inputs(wrapper.element.parentElement!)[0]!;
expect(input.style.position).toBe('absolute');
expect(input.getAttribute('aria-hidden')).toBe('true');
expect(input.getAttribute('tabindex')).toBe('-1');
expect(input.getAttribute('data-visually-hidden')).toBe('hidden');
});
it('serializes an array of primitives to name[index]', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'tags', value: ['a', 'b', 'c'] },
});
const all = inputs(wrapper.element.parentElement!);
expect(all.map(i => i.name)).toEqual(['tags[0]', 'tags[1]', 'tags[2]']);
expect(all.map(i => i.value)).toEqual(['a', 'b', 'c']);
});
it('serializes an array of objects to name[index][key]', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'items', value: [{ id: 1, label: 'x' }, { id: 2, label: 'y' }] },
});
const all = inputs(wrapper.element.parentElement!);
expect(all.map(i => i.name)).toEqual([
'items[0][id]',
'items[0][label]',
'items[1][id]',
'items[1][label]',
]);
expect(all.map(i => i.value)).toEqual(['1', 'x', '2', 'y']);
});
it('serializes a plain object to name[key]', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'coords', value: { x: 10, y: 20 } },
});
const all = inputs(wrapper.element.parentElement!);
expect(all.map(i => i.name)).toEqual(['coords[x]', 'coords[y]']);
expect(all.map(i => i.value)).toEqual(['10', '20']);
});
it('renders nothing for an empty, non-required array', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'tags', value: [] },
});
expect(inputs(wrapper.element.parentElement!)).toHaveLength(0);
});
it('renders one required input for an empty required array (native validation fires)', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'tags', value: [], required: true },
});
const all = inputs(wrapper.element.parentElement!);
expect(all).toHaveLength(1);
expect(all[0]!.name).toBe('tags');
expect(all[0]!.required).toBe(true);
});
it('forwards required and disabled to every leaf input', () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'tags', value: ['a', 'b'], required: true, disabled: true },
});
for (const input of inputs(wrapper.element.parentElement!)) {
expect(input.required).toBe(true);
expect(input.disabled).toBe(true);
}
});
it('updates the rendered inputs when the array value changes', async () => {
wrapper = mount(VisuallyHiddenInput, {
attachTo: document.body,
props: { name: 'tags', value: ['a'] },
});
expect(inputs(wrapper.element.parentElement!)).toHaveLength(1);
await wrapper.setProps({ value: ['a', 'b'] });
await nextTick();
const all = inputs(wrapper.element.parentElement!);
expect(all.map(i => i.name)).toEqual(['tags[0]', 'tags[1]']);
});
});
describe('VisuallyHiddenInputBubble', () => {
let wrapper: ReturnType<typeof mount> | undefined;
afterEach(() => {
wrapper?.unmount();
wrapper = undefined;
});
it('renders a hidden text input mirroring the value', () => {
wrapper = mount(VisuallyHiddenInputBubble, {
attachTo: document.body,
props: { name: 'q', value: 'hello' },
});
const input = wrapper.element as HTMLInputElement;
expect(input.tagName).toBe('INPUT');
expect(input.type).toBe('text');
expect(input.name).toBe('q');
expect(input.value).toBe('hello');
});
it('renders a checkbox-style input when checked is provided', () => {
wrapper = mount(VisuallyHiddenInputBubble, {
attachTo: document.body,
props: { name: 'agree', value: 'on', checked: true },
});
const input = wrapper.element as HTMLInputElement;
expect(input.type).toBe('checkbox');
expect(input.checked).toBe(true);
});
it('dispatches bubbling input and change events when the value changes programmatically', async () => {
const onInput = vi.fn();
const onChange = vi.fn();
document.body.addEventListener('input', onInput);
document.body.addEventListener('change', onChange);
wrapper = mount(VisuallyHiddenInputBubble, {
attachTo: document.body,
props: { name: 'q', value: 'a' },
});
onInput.mockClear();
onChange.mockClear();
await wrapper.setProps({ value: 'b' });
await nextTick();
expect((wrapper.element as HTMLInputElement).value).toBe('b');
expect(onInput).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledTimes(1);
document.body.removeEventListener('input', onInput);
document.body.removeEventListener('change', onChange);
});
it('dispatches change events when the checked state changes programmatically', async () => {
const onChange = vi.fn();
document.body.addEventListener('change', onChange);
wrapper = mount(VisuallyHiddenInputBubble, {
attachTo: document.body,
props: { name: 'agree', value: 'on', checked: false },
});
onChange.mockClear();
await wrapper.setProps({ checked: true });
await nextTick();
expect((wrapper.element as HTMLInputElement).checked).toBe(true);
expect(onChange).toHaveBeenCalledTimes(1);
document.body.removeEventListener('change', onChange);
});
it('does not dispatch when the value is unchanged', async () => {
const onChange = vi.fn();
document.body.addEventListener('change', onChange);
wrapper = mount(VisuallyHiddenInputBubble, {
attachTo: document.body,
props: { name: 'q', value: 'a' },
});
onChange.mockClear();
await wrapper.setProps({ value: 'a' });
await nextTick();
expect(onChange).not.toHaveBeenCalled();
document.body.removeEventListener('change', onChange);
});
});