From d093923a63486104ed6cc3621a5d6ae43f244cce Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 15:17:19 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20composant=20MalioDateWeek=20(s=C3=A9?= =?UTF-8?q?lection=20semaine=20ISO)=20(#MUI-33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/malio/date/DateWeek.test.ts | 122 ++++++++++++++++++++ app/components/malio/date/DateWeek.vue | 123 +++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 app/components/malio/date/DateWeek.test.ts create mode 100644 app/components/malio/date/DateWeek.vue diff --git a/app/components/malio/date/DateWeek.test.ts b/app/components/malio/date/DateWeek.test.ts new file mode 100644 index 0000000..613c749 --- /dev/null +++ b/app/components/malio/date/DateWeek.test.ts @@ -0,0 +1,122 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import DateWeek from './DateWeek.vue' + +type DateWeekProps = { + modelValue?: string | null + label?: string + disabled?: boolean + readonly?: boolean + error?: string + min?: string + max?: string +} + +const DateWeekForTest = DateWeek as DefineComponent +const mountWeek = (props: DateWeekProps = {}) => + mount(DateWeekForTest, {props, attachTo: document.body}) + +describe('MalioDateWeek', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026 + }) + afterEach(() => vi.useRealTimers()) + + it('renders the label and calendar icon', () => { + const wrapper = mountWeek({label: 'Semaine'}) + expect(wrapper.get('label').text()).toBe('Semaine') + expect(wrapper.find('[data-test="calendar-icon"]').exists()).toBe(true) + }) + + it('displays the formatted week when modelValue is set', () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement + expect(input.value).toBe('Semaine 21 (18/05 → 24/05/2026)') + }) + + it('shows an empty field without a value', () => { + const wrapper = mountWeek() + const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement + expect(input.value).toBe('') + }) + + it('opens on the month of the selected week', async () => { + const wrapper = mountWeek({modelValue: '2026-W01'}) // lundi 2025-12-29 + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Décembre 2025') + }) + + it('selects the week when a day is clicked', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="day"][data-iso="2026-05-20"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-W21']) + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('selects the week when the week number is clicked', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-W21']) + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('previews the whole week on day hover', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="day"][data-iso="2026-05-20"]').trigger('mouseenter') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-18"]').attributes('data-range-role')).toBe('start') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-24"]').attributes('data-range-role')).toBe('end') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-20"]').attributes('data-range-role')).toBe('in-range') + }) + + it('previews the whole week on week-number hover', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').trigger('mouseenter') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-22"]').attributes('data-range-role')).toBe('in-range') + }) + + it('marks the committed week number', async () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').attributes('data-marked')).toBe('true') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-18"]').attributes('data-range-role')).toBe('start') + }) + + it('emits null on clear', async () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + await wrapper.get('[data-test="clear"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null]) + }) + + it('disables a week fully outside min/max', async () => { + const wrapper = mountWeek({min: '2026-05-18', max: '2026-05-31'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + const earlyWeek = wrapper.get('[data-test="week-number"][data-week-start="2026-05-11"]') + expect((earlyWeek.element as HTMLButtonElement).disabled).toBe(true) + const selectableWeek = wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]') + expect((selectableWeek.element as HTMLButtonElement).disabled).toBe(false) + }) + + it('does not open when disabled', async () => { + const wrapper = mountWeek({disabled: true}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('does not open when readonly', async () => { + const wrapper = mountWeek({readonly: true, modelValue: '2026-W21'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('sets aria-invalid on error', () => { + const wrapper = mountWeek({error: 'Semaine requise'}) + expect(wrapper.get('[data-test="date-input"]').attributes('aria-invalid')).toBe('true') + expect(wrapper.text()).toContain('Semaine requise') + }) +}) diff --git a/app/components/malio/date/DateWeek.vue b/app/components/malio/date/DateWeek.vue new file mode 100644 index 0000000..4230e08 --- /dev/null +++ b/app/components/malio/date/DateWeek.vue @@ -0,0 +1,123 @@ + + +