From 9207d7bb957af5fef1b8304c80a8f4fca5a7c7b4 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 24 Jun 2026 16:21:34 +0200 Subject: [PATCH] feat(radio) : contexte de groupe injectable pour RadioButton Co-Authored-By: Claude Sonnet 4.6 --- .../malio/radio/RadioButton.test.ts | 68 ++++++++++++++++- app/components/malio/radio/RadioButton.vue | 74 ++++++++++++------- app/components/malio/radio/context.ts | 17 +++++ 3 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 app/components/malio/radio/context.ts diff --git a/app/components/malio/radio/RadioButton.test.ts b/app/components/malio/radio/RadioButton.test.ts index 173bf65..c66dbbc 100644 --- a/app/components/malio/radio/RadioButton.test.ts +++ b/app/components/malio/radio/RadioButton.test.ts @@ -1,7 +1,9 @@ -import {describe, expect, it} from 'vitest' +import {computed, ref} from 'vue' +import {describe, expect, it, vi} from 'vitest' import {mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' import RadioButton from './RadioButton.vue' +import {radioGroupContextKey, type RadioGroupContext, type RadioValue} from './context' type RadioButtonProps = { id?: string @@ -193,3 +195,67 @@ describe('MalioRadioButton', () => { expect(wrapper.get('.radio-text').classes()).toContain('text-black') }) }) + +const makeGroupCtx = (over: Partial<{ + selected: RadioValue; error: boolean; success: boolean + disabled: boolean; readonly: boolean; required: boolean +}> = {}) => { + const selected = ref(over.selected ?? null) + const select = vi.fn((v: RadioValue) => { selected.value = v }) + const ctx: RadioGroupContext = { + name: computed(() => 'grp'), + isSelected: (v) => selected.value === v, + select, + hasError: computed(() => !!over.error), + hasSuccess: computed(() => !!over.success), + disabled: computed(() => !!over.disabled), + readonly: computed(() => !!over.readonly), + required: computed(() => !!over.required), + describedBy: computed(() => 'grp-describedby'), + } + return {ctx, select, selected} +} + +const mountInGroup = (props: RadioButtonProps, ctx: RadioGroupContext) => + mount(RadioButtonForTest, { + props, + global: {provide: {[radioGroupContextKey as symbol]: ctx}}, + }) + +describe('MalioRadioButton dans un groupe', () => { + it('hérite du name du groupe', () => { + const {ctx} = makeGroupCtx() + const wrapper = mountInGroup({value: 'a', label: 'A'}, ctx) + expect(wrapper.get('input').attributes('name')).toBe('grp') + }) + + it('coché selon isSelected du groupe', () => { + const {ctx} = makeGroupCtx({selected: 'a'}) + const wrapper = mountInGroup({value: 'a', label: 'A'}, ctx) + expect((wrapper.get('input').element as HTMLInputElement).checked).toBe(true) + }) + + it('appelle ctx.select au change au lieu d\'émettre', async () => { + const {ctx, select} = makeGroupCtx() + const wrapper = mountInGroup({value: 'b', label: 'B'}, ctx) + await wrapper.get('input').trigger('change') + expect(select).toHaveBeenCalledWith('b') + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + }) + + it('reflète l\'erreur du groupe et ne rend aucun message propre', () => { + const {ctx} = makeGroupCtx({error: true}) + const wrapper = mountInGroup({value: 'a', label: 'A', hint: 'ignoré'}, ctx) + expect(wrapper.get('.radio-control').classes()).toContain('is-error') + expect(wrapper.get('input').attributes('aria-invalid')).toBe('true') + expect(wrapper.find('.radio-message').exists()).toBe(false) + expect(wrapper.get('input').attributes('aria-describedby')).toBe('grp-describedby') + }) + + it('hérite de disabled/required du groupe', () => { + const {ctx} = makeGroupCtx({disabled: true, required: true}) + const wrapper = mountInGroup({value: 'a', label: 'A'}, ctx) + expect(wrapper.get('input').attributes('disabled')).toBeDefined() + expect(wrapper.get('input').attributes('required')).toBeDefined() + }) +}) diff --git a/app/components/malio/radio/RadioButton.vue b/app/components/malio/radio/RadioButton.vue index 7c33e9d..a1dc597 100644 --- a/app/components/malio/radio/RadioButton.vue +++ b/app/components/malio/radio/RadioButton.vue @@ -1,15 +1,15 @@