diff --git a/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js b/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js index 5feeefbddbc..1390858ea48 100644 --- a/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js +++ b/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js @@ -5,8 +5,10 @@ import { z } from 'zod' import CheckboxGroup from '..' import Checkbox from '../../checkbox' import VarCheckbox from '../../checkbox/Checkbox' +import VarForm from '../../form' import { delay, trigger, triggerKeyboard } from '../../utils/test' import VarCheckboxGroup from '../CheckboxGroup' +import { CHECKBOX_GROUP_BIND_CHECKBOX_KEY } from '../provide' test('checkbox group plugin', () => { const app = createApp({}).use(CheckboxGroup) @@ -314,6 +316,103 @@ test('checkbox group max', async () => { wrapper.unmount() }) +test('checkbox group default modelValue is empty array', () => { + const wrapper = mount(VarCheckboxGroup) + + expect(wrapper.props('modelValue')).toStrictEqual([]) + + wrapper.unmount() +}) + +test('checkbox group ignores unchecked value not in modelValue', async () => { + const wrapper = mount({ + components: { + [VarCheckboxGroup.name]: VarCheckboxGroup, + [VarCheckbox.name]: VarCheckbox, + }, + template: ` + + + + `, + }) + + const checkbox = wrapper.find('.var-checkbox') + await checkbox.trigger('click') + await checkbox.trigger('click') + + expect(wrapper.findComponent(VarCheckboxGroup).props('modelValue')).toStrictEqual([]) + + wrapper.unmount() +}) + +test('checkbox group early return when unchecked value not in modelValue', async () => { + const wrapper = mount({ + components: { + [VarCheckboxGroup.name]: VarCheckboxGroup, + [VarCheckbox.name]: VarCheckbox, + }, + template: ` + + + + `, + }) + + const checkbox = wrapper.findComponent(VarCheckbox) + checkbox.vm.toggle(false) + await wrapper.vm.$nextTick() + + expect(wrapper.findComponent(VarCheckboxGroup).props('modelValue')).toStrictEqual([]) + + wrapper.unmount() +}) + +test('checkbox group ignores checked value already in modelValue', async () => { + const wrapper = mount({ + components: { + [VarCheckboxGroup.name]: VarCheckboxGroup, + [VarCheckbox.name]: VarCheckbox, + }, + data: () => ({ + value: [1], + }), + template: ` + + + + `, + }) + + const checkbox = wrapper.findComponent(VarCheckbox) + checkbox.vm.toggle(1) + await wrapper.vm.$nextTick() + + expect(wrapper.vm.value).toStrictEqual([1]) + + wrapper.unmount() +}) + +test('checkbox group onChecked branches', () => { + const onUpdateModelValue = vi.fn() + const wrapper = mount(VarCheckboxGroup, { + props: { + modelValue: [1], + 'onUpdate:modelValue': onUpdateModelValue, + }, + }) + + const provider = wrapper.vm.$.provides[CHECKBOX_GROUP_BIND_CHECKBOX_KEY] + + provider.onChecked(1) + expect(onUpdateModelValue).toHaveBeenCalledTimes(0) + + provider.onChecked(2) + expect(onUpdateModelValue).toHaveBeenCalledWith([1, 2]) + + wrapper.unmount() +}) + test('checkbox group validation', async () => { const wrapper = mount({ components: { @@ -705,3 +804,329 @@ describe('validation with zod', () => { wrapper.unmount() }) }) + +test('checkbox focus and blur updates focusing state', async () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + const checkbox = wrapper.find('.var-checkbox') + + await checkbox.trigger('focus') + expect(wrapper.vm.isFocusing).toBe(true) + + await checkbox.trigger('blur') + expect(wrapper.vm.isFocusing).toBe(false) + + wrapper.unmount() +}) + +test('checkbox tabindex defaults and respects disabled/custom values', async () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + expect(wrapper.find('.var-checkbox').attributes('tabindex')).toBe('0') + + await wrapper.setProps({ disabled: true }) + expect(wrapper.find('.var-checkbox').attributes('tabindex')).toBeUndefined() + + await wrapper.setProps({ disabled: false, tabindex: '-1' }) + expect(wrapper.find('.var-checkbox').attributes('tabindex')).toBe('-1') + + wrapper.unmount() +}) + +test('checkbox text click focuses checkbox', async () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + slots: { + default: 'Label', + }, + }) + + const focus = vi.spyOn(wrapper.vm.checkbox, 'focus') + await wrapper.find('.var-checkbox__text').trigger('click') + + expect(focus).toHaveBeenCalledTimes(1) + + wrapper.unmount() +}) + +test('checkbox renders icons for checked, unchecked, and indeterminate states', async () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + expect(wrapper.find('.var-icon-checkbox-blank-outline').exists()).toBe(true) + + await wrapper.setProps({ modelValue: true }) + expect(wrapper.find('.var-icon-checkbox-marked').exists()).toBe(true) + + await wrapper.setProps({ modelValue: false, indeterminate: true }) + expect(wrapper.find('.var-icon-minus-box').exists()).toBe(true) + + wrapper.unmount() +}) + +test('checkbox disabled prevents click side effects', async () => { + const onClick = vi.fn() + const onChange = vi.fn() + const onUpdateModelValue = vi.fn() + + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + disabled: true, + onClick, + onChange, + 'onUpdate:modelValue': onUpdateModelValue, + }, + }) + + await wrapper.find('.var-checkbox').trigger('click') + + expect(onClick).toHaveBeenCalledTimes(0) + expect(onChange).toHaveBeenCalledTimes(0) + expect(onUpdateModelValue).toHaveBeenCalledTimes(0) + + wrapper.unmount() +}) + +test('checkbox readonly allows click but prevents change', async () => { + const onClick = vi.fn() + const onChange = vi.fn() + + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + readonly: true, + onClick, + onChange, + 'onUpdate:modelValue': (value) => wrapper.setProps({ modelValue: value }), + }, + }) + + await wrapper.find('.var-checkbox').trigger('click') + + expect(onClick).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledTimes(0) + expect(wrapper.props('modelValue')).toBe(false) + + wrapper.unmount() +}) + +test('checkbox indeterminate click clears indeterminate without toggling value', async () => { + const onChange = vi.fn() + + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + indeterminate: true, + onChange, + 'onUpdate:indeterminate': (value) => wrapper.setProps({ indeterminate: value }), + }, + }) + + await wrapper.find('.var-checkbox').trigger('click') + + expect(onChange).toHaveBeenCalledWith(false, false) + expect(wrapper.props('indeterminate')).toBe(false) + expect(wrapper.props('modelValue')).toBe(false) + + wrapper.unmount() +}) + +test('checkbox respects form disabled state', async () => { + const wrapper = mount({ + components: { + [VarForm.name]: VarForm, + [VarCheckbox.name]: VarCheckbox, + }, + data: () => ({ + value: false, + }), + template: ` + + + + `, + }) + + await wrapper.find('.var-checkbox').trigger('click') + expect(wrapper.vm.value).toBe(false) + + wrapper.unmount() +}) + +test('checkbox group max prevents checking new value', async () => { + const wrapper = mount({ + components: { + [VarCheckboxGroup.name]: VarCheckboxGroup, + [VarCheckbox.name]: VarCheckbox, + }, + data: () => ({ + value: [1], + }), + template: ` + + + + + `, + }) + + const checkboxes = wrapper.findAll('.var-checkbox') + await checkboxes[1].trigger('click') + + expect(wrapper.vm.value).toStrictEqual([1]) + + wrapper.unmount() +}) + +test('checkbox keyboard interactions trigger click when focused', async () => { + const addEventListener = vi.spyOn(window, 'addEventListener') + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + const click = vi.spyOn(wrapper.vm.checkbox, 'click') + + wrapper.vm.isFocusing = true + await wrapper.vm.$nextTick() + + const keydownHandler = addEventListener.mock.calls.find(([type]) => type === 'keydown')?.[1] + const keyupHandler = addEventListener.mock.calls.find(([type]) => type === 'keyup')?.[1] + + keydownHandler?.(new KeyboardEvent('keydown', { key: 'Enter', cancelable: true })) + keyupHandler?.(new KeyboardEvent('keyup', { key: ' ', cancelable: true })) + + expect(click).toHaveBeenCalledTimes(2) + + wrapper.unmount() + addEventListener.mockRestore() +}) + +test('checkbox keyboard handlers prevent default and click as expected', async () => { + const addEventListener = vi.spyOn(window, 'addEventListener') + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + const click = vi.spyOn(wrapper.vm.checkbox, 'click') + + wrapper.vm.isFocusing = true + await wrapper.vm.$nextTick() + + const keydownHandler = addEventListener.mock.calls.find(([type]) => type === 'keydown')?.[1] + const keyupHandler = addEventListener.mock.calls.find(([type]) => type === 'keyup')?.[1] + + const keydownSpace = new KeyboardEvent('keydown', { key: ' ', cancelable: true }) + keydownHandler?.(keydownSpace) + expect(keydownSpace.defaultPrevented).toBe(true) + + const keydownEnter = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true }) + keydownHandler?.(keydownEnter) + expect(keydownEnter.defaultPrevented).toBe(true) + expect(click).toHaveBeenCalledTimes(1) + + const keyupSpace = new KeyboardEvent('keyup', { key: ' ', cancelable: true }) + keyupHandler?.(keyupSpace) + expect(keyupSpace.defaultPrevented).toBe(true) + expect(click).toHaveBeenCalledTimes(2) + + wrapper.unmount() + addEventListener.mockRestore() +}) + +test('checkbox keyboard events do nothing when not focused', () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + const click = vi.spyOn(wrapper.vm.checkbox, 'click') + + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', cancelable: true })) + window.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', cancelable: true })) + + expect(click).toHaveBeenCalledTimes(0) + + wrapper.unmount() +}) + +test('checkbox keyboard handlers ignore other keys when focused', async () => { + const addEventListener = vi.spyOn(window, 'addEventListener') + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + }, + }) + + const click = vi.spyOn(wrapper.vm.checkbox, 'click') + + wrapper.vm.isFocusing = true + await wrapper.vm.$nextTick() + + const keydownHandler = addEventListener.mock.calls.find(([type]) => type === 'keydown')?.[1] + const keyupHandler = addEventListener.mock.calls.find(([type]) => type === 'keyup')?.[1] + + const keydownOther = new KeyboardEvent('keydown', { key: 'a', cancelable: true }) + keydownHandler?.(keydownOther) + expect(keydownOther.defaultPrevented).toBe(false) + + const keyupOther = new KeyboardEvent('keyup', { key: 'a', cancelable: true }) + keyupHandler?.(keyupOther) + expect(keyupOther.defaultPrevented).toBe(false) + + expect(click).toHaveBeenCalledTimes(0) + + wrapper.unmount() + addEventListener.mockRestore() +}) + +test('checkbox toggle, reset, and validate behave as expected', async () => { + const wrapper = mount(VarCheckbox, { + props: { + modelValue: false, + rules: [(value) => (value ? true : 'Required')], + 'onUpdate:modelValue': (value) => wrapper.setProps({ modelValue: value }), + }, + }) + + wrapper.vm.toggle('invalid') + await wrapper.vm.$nextTick() + expect(wrapper.props('modelValue')).toBe(true) + + wrapper.vm.toggle(false) + await wrapper.vm.$nextTick() + expect(wrapper.props('modelValue')).toBe(false) + + await wrapper.setProps({ modelValue: true }) + wrapper.vm.toggle() + await wrapper.vm.$nextTick() + expect(wrapper.props('modelValue')).toBe(false) + + const valid = await wrapper.vm.validate() + expect(valid).toBe(false) + expect(wrapper.vm.errorMessage).toBe('Required') + + wrapper.vm.reset() + expect(wrapper.props('modelValue')).toBe(false) + expect(wrapper.vm.errorMessage).toBe('') + + wrapper.unmount() +})