faafd99ef8
Auto Tag Develop / tag (push) Successful in 8s
MR unique regroupant tout le module M5 « Tickets de pesée » (remplace les MR empilées #140/#141/#142/#143).
## Périmètre
- **ERP-188** — Page liste des tickets de pesée + export XLSX (colonnes Fournisseur/Client/Autre + Statut).
- **ERP-189** — Écran « Ajouter » (4 champs en haut, 2 blocs de pesée, pesée bascule/manuelle, date+heure horodatée à la validation).
- **ERP-190** — Écran « Modifier » + bouton Imprimer.
- **ERP-191** — i18n + libellés + branchement site courant.
- **ERP-192** — Bon de pesée PDF généré côté back (template Twig → Dompdf), endpoint `GET /api/weighing_tickets/{id}/print.pdf`.
- **ERP-193** — Cycle de vie brouillon/validé (status DRAFT/VALIDATED, numéro attribué à la validation), DSD saisi conservé en pesée manuelle, retours métier design.
## Vérifications
- Back : tests Logistique + architecture verts, php-cs-fixer propre, migrations appliquées (dev + test).
- Front : suite Vitest complète verte, ESLint propre.
Base : `develop` — contient les 16 commits du M5 (rien d'autre).
Reviewed-on: #144
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
145 lines
6.7 KiB
TypeScript
145 lines
6.7 KiB
TypeScript
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')
|
|
})
|
|
})
|