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()
+})