import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { defineComponent, h, ref, reactive, Suspense } from 'vue' // ── Mocks des composables modules (le form RÉEL est conservé pour vérifier le // pré-remplissage via hydrate). ───────────────────────────────────────────── const mockFetchTicket = vi.hoisted(() => vi.fn()) const mockPatch = vi.hoisted(() => vi.fn()) const mockPush = vi.hoisted(() => vi.fn()) const mockOpen = vi.hoisted(() => vi.fn()) vi.mock('~/modules/logistique/composables/useWeighingTicket', () => ({ useWeighingTicket: () => ({ fetchTicket: mockFetchTicket }), })) vi.mock('~/modules/logistique/composables/useWeighingTicketReferentials', () => ({ useWeighingTicketReferentials: () => ({ clients: ref([]), suppliers: ref([]), load: vi.fn().mockResolvedValue(undefined) }), })) vi.mock('~/modules/logistique/composables/useWeighbridge', () => ({ useWeighbridge: () => ({ triggerAuto: vi.fn(), triggerManual: vi.fn(), extractWeighbridgeError: () => 'err' }), })) // ── Auto-imports Nuxt stubbes globalement ─────────────────────────────────── vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) vi.stubGlobal('useHead', () => undefined) vi.stubGlobal('useApi', () => ({ get: vi.fn(), post: vi.fn(), patch: mockPatch })) vi.stubGlobal('useRoute', () => ({ params: { id: '9' } })) vi.stubGlobal('useRouter', () => ({ push: mockPush })) vi.stubGlobal('usePermissions', () => ({ can: () => true })) vi.stubGlobal('navigateTo', vi.fn()) vi.stubGlobal('useFormErrors', () => ({ errors: reactive({}), setError: vi.fn(), clearErrors: vi.fn(), handleApiError: vi.fn() })) globalThis.open = mockOpen const EditPage = (await import('../weighing-tickets/[id]/edit.vue')).default // ── Stubs de composants ────────────────────────────────────────────────────── const ButtonStub = defineComponent({ props: { label: { type: String, default: '' }, disabled: { type: Boolean, default: false } }, emits: ['click'], setup(props, { emit }) { return () => h('button', { 'data-label': props.label, onClick: () => emit('click') }, props.label) }, }) const InputStub = defineComponent({ props: { label: { type: String, default: '' }, modelValue: { default: null } }, setup(props) { return () => h('input', { 'data-label': props.label, 'value': props.modelValue as string }) }, }) // WeighingBlock stubbé (Date/Poids/DSD + boutons) — la contrepartie vit désormais // dans les 4 champs du haut, hors bloc (ERP-193). const BlockStub = defineComponent({ setup() { return () => h('div', { 'data-testid': 'block' }) }, }) const ModalStub = defineComponent({ props: { modelValue: { type: Boolean, default: false } }, setup(_, { slots }) { return () => h('div', {}, [slots.header?.(), slots.default?.(), slots.footer?.()]) }, }) const stubs = { MalioButtonIcon: ButtonStub, MalioButton: ButtonStub, MalioInputText: InputStub, MalioInputNumber: InputStub, MalioSelect: InputStub, MalioDateTime: InputStub, MalioCheckbox: InputStub, MalioModal: ModalStub, WeighingBlock: BlockStub, } // Monte la page (setup async : top-level await) via Suspense. async function mountPage() { const wrapper = mount(defineComponent({ components: { EditPage }, setup: () => () => h(Suspense, null, { default: () => h(EditPage) }), }), { global: { stubs } }) await flushPromises() return wrapper } const DETAIL = { id: 9, status: 'VALIDATED', number: '86-TP-0001', site: { id: 1, name: 'Chatellerault', code: '86' }, counterpartyType: 'CLIENT', client: { '@id': '/api/clients/629', companyName: 'NÉGOCE MÉTAUX ATLANTIQUE' }, immatriculation: 'AB-123-CD', plateFreeFormat: false, emptyDate: '2026-06-17T09:00:00+02:00', emptyWeight: 7150, emptyDsd: 1, emptyMode: 'AUTO', fullDate: '2026-06-17T09:12:00+02:00', fullWeight: 14300, fullDsd: 2, fullMode: 'AUTO', } describe('Écran Modification ticket de pesée (page /weighing-tickets/{id}/edit)', () => { beforeEach(() => { mockFetchTicket.mockReset().mockResolvedValue({ ...DETAIL }) mockPatch.mockReset().mockResolvedValue({}) mockPush.mockReset() mockOpen.mockReset() }) it('charge le ticket au montage (pré-remplissage via hydrate)', async () => { await mountPage() expect(mockFetchTicket).toHaveBeenCalledWith('9') }) it('ticket validé : action principale « Enregistrer » + « Imprimer » (pas « Valider »)', async () => { const wrapper = await mountPage() // DETAIL.status = VALIDATED → l'action principale s'intitule « Enregistrer ». expect(wrapper.find('[data-label="logistique.weighingTickets.form.save"]').exists()).toBe(true) expect(wrapper.find('[data-label="logistique.weighingTickets.form.print"]').exists()).toBe(true) expect(wrapper.find('[data-label="logistique.weighingTickets.form.validate"]').exists()).toBe(false) }) it('« Imprimer » ouvre le bon de pesée PDF servi par le back (RG-5.08)', async () => { const wrapper = await mountPage() await wrapper.find('[data-label="logistique.weighingTickets.form.print"]').trigger('click') expect(mockOpen).toHaveBeenCalledWith('/api/weighing_tickets/9/print.pdf', '_blank') }) it('« Enregistrer » : PATCH brouillon puis PATCH /validate, retour à la liste', async () => { const wrapper = await mountPage() await wrapper.find('[data-label="logistique.weighingTickets.form.save"]').trigger('click') await flushPromises() // 1. Persistance de l'état courant (brouillon) avec les 2 pesées. expect(mockPatch).toHaveBeenCalledWith( '/weighing_tickets/9', expect.objectContaining({ counterpartyType: 'CLIENT', client: '/api/clients/629', fullWeight: 14300 }), expect.objectContaining({ toast: false }), ) // 2. Validation (back autoritaire) — ne porte que les 4 champs du haut. expect(mockPatch).toHaveBeenCalledWith( '/weighing_tickets/9/validate', expect.objectContaining({ counterpartyType: 'CLIENT', immatriculation: 'AB-123-CD' }), expect.objectContaining({ toast: false }), ) // « Enregistrer » ouvre aussi le bon de pesée PDF (RG-5.08). expect(mockOpen).toHaveBeenCalledWith('/api/weighing_tickets/9/print.pdf', '_blank') expect(mockPush).toHaveBeenCalledWith('/weighing-tickets') }) })