feat(catalog) : M6 — écran Modification produit + onglets placeholder (ERP-206)
Écran de modification (ajout pré-rempli, bouton « Enregistrer ») et pose des
onglets Fournisseurs/Clients en placeholder « en cours de développement ».
- route /admin/products/{id}/edit : useProduct(id) charge le détail, prefill du formulaire principal
- RG-6.08 : useProductForm en mode édition → PATCH /products/{id} (merge-patch), bouton « Enregistrer »
- unicité du code re-validée serveur en édition (409 doublon mappé inline)
- onglets Fournisseurs + Clients : ComingSoonPlaceholder, aucun appel API ni champ (HP-M6-01 / RG-6.10)
- mêmes onglets placeholder posés sur l'écran Ajouter (cohérence)
- i18n admin.products.edit / tab ; 11 tests Vitest (prefill + PATCH + placeholder)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { defineComponent, h, Suspense } from 'vue'
|
||||
|
||||
// Produit charge simule (cles de la reponse reelle § 4.0.bis).
|
||||
const PRODUCT = {
|
||||
id: 34,
|
||||
code: 'BLE-01',
|
||||
name: 'Blé tendre',
|
||||
states: ['PURCHASE'],
|
||||
manufactured: false,
|
||||
containsMolasses: false,
|
||||
category: { '@id': '/api/categories/12', id: 12, name: 'Céréales', code: 'CEREALES' },
|
||||
sites: [{ '@id': '/api/sites/1', id: 1, name: 'Chatellerault' }],
|
||||
storageTypes: [{ '@id': '/api/storage_types/9', id: 9, code: 'TAS', label: 'Tas' }],
|
||||
createdAt: '', updatedAt: '',
|
||||
}
|
||||
|
||||
// Holders crees dans les factories (vue initialise au moment de l'import page).
|
||||
const fx = vi.hoisted(() => ({
|
||||
isSale: null as unknown as { value: boolean },
|
||||
submit: vi.fn(),
|
||||
prefill: vi.fn(),
|
||||
loadReferentials: vi.fn(),
|
||||
load: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('~/modules/catalog/composables/useProductForm', async () => {
|
||||
const { ref, reactive } = await import('vue')
|
||||
fx.isSale = ref(false)
|
||||
return {
|
||||
PRODUCT_STATES: ['PURCHASE', 'SALE', 'OTHER'],
|
||||
useProductForm: () => ({
|
||||
form: reactive({
|
||||
code: null, name: null, states: [], siteIris: [],
|
||||
categoryIri: null, storageTypeIris: [], manufactured: false, containsMolasses: false,
|
||||
}),
|
||||
errors: reactive({}),
|
||||
submitting: ref(false),
|
||||
isSale: fx.isSale,
|
||||
siteOptions: ref([]),
|
||||
categoryOptions: ref([]),
|
||||
storageTypeOptions: ref([]),
|
||||
setStates: vi.fn(),
|
||||
setCategory: vi.fn(),
|
||||
setStorageTypes: vi.fn(),
|
||||
setSites: vi.fn(),
|
||||
loadReferentials: fx.loadReferentials,
|
||||
prefill: fx.prefill,
|
||||
submit: fx.submit,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('~/modules/catalog/composables/useProduct', async () => {
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useProduct: () => ({
|
||||
product: ref(PRODUCT),
|
||||
loading: ref(false),
|
||||
error: ref(false),
|
||||
load: fx.load,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
// ── Auto-imports Nuxt stubbes globalement ───────────────────────────────────
|
||||
const mockPush = vi.hoisted(() => vi.fn())
|
||||
const mockNavigateTo = vi.hoisted(() => vi.fn())
|
||||
const mockCan = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
||||
vi.stubGlobal('useHead', () => undefined)
|
||||
vi.stubGlobal('useRoute', () => ({ params: { id: '34' } }))
|
||||
vi.stubGlobal('useRouter', () => ({ push: mockPush }))
|
||||
vi.stubGlobal('usePermissions', () => ({ can: mockCan }))
|
||||
vi.stubGlobal('navigateTo', mockNavigateTo)
|
||||
|
||||
const EditPage = (await import('../admin/products/[id]/edit.vue')).default
|
||||
|
||||
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 }) },
|
||||
})
|
||||
const CheckboxStub = defineComponent({
|
||||
props: { label: { type: String, default: '' } },
|
||||
setup(props) { return () => h('input', { 'type': 'checkbox', 'data-label': props.label }) },
|
||||
})
|
||||
// Placeholder : rendu sans aucun appel API (juste un marqueur).
|
||||
const TabsStub = defineComponent({ setup() { return () => h('div', { 'data-testid': 'placeholder-tabs' }) } })
|
||||
|
||||
const stubs = {
|
||||
MalioButtonIcon: ButtonStub,
|
||||
MalioButton: ButtonStub,
|
||||
MalioInputText: InputStub,
|
||||
MalioSelect: InputStub,
|
||||
MalioSelectCheckbox: InputStub,
|
||||
MalioCheckbox: CheckboxStub,
|
||||
ProductPlaceholderTabs: TabsStub,
|
||||
}
|
||||
|
||||
async function mountPage() {
|
||||
const wrapper = mount(defineComponent({
|
||||
components: { EditPage },
|
||||
setup: () => () => h(Suspense, null, { default: () => h(EditPage) }),
|
||||
}), { global: { stubs } })
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('Écran Modifier un produit (page /admin/products/{id}/edit)', () => {
|
||||
beforeEach(() => {
|
||||
fx.submit.mockReset().mockResolvedValue(true)
|
||||
fx.prefill.mockReset().mockResolvedValue(undefined)
|
||||
fx.loadReferentials.mockReset().mockResolvedValue(undefined)
|
||||
fx.load.mockReset().mockResolvedValue(undefined)
|
||||
mockPush.mockReset()
|
||||
mockNavigateTo.mockReset()
|
||||
mockCan.mockReset().mockReturnValue(true)
|
||||
fx.isSale.value = false
|
||||
})
|
||||
|
||||
it('charge le produit et pre-remplit le formulaire au montage', async () => {
|
||||
await mountPage()
|
||||
expect(fx.load).toHaveBeenCalled()
|
||||
expect(fx.prefill).toHaveBeenCalledWith(PRODUCT)
|
||||
})
|
||||
|
||||
it('redirige vers la liste sans la permission manage', async () => {
|
||||
mockCan.mockReturnValue(false)
|
||||
await mountPage()
|
||||
expect(mockNavigateTo).toHaveBeenCalledWith('/admin/products')
|
||||
})
|
||||
|
||||
it('bouton « Enregistrer » : submit (PATCH) puis retour a la liste', async () => {
|
||||
const wrapper = await mountPage()
|
||||
await wrapper.find('[data-label="admin.products.edit.save"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(fx.submit).toHaveBeenCalled()
|
||||
expect(mockPush).toHaveBeenCalledWith('/admin/products')
|
||||
})
|
||||
|
||||
it('affiche les onglets placeholder (rendu sans appel API)', async () => {
|
||||
const wrapper = await mountPage()
|
||||
expect(wrapper.find('[data-testid="placeholder-tabs"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -67,6 +67,9 @@ const CheckboxStub = defineComponent({
|
||||
setup(props) { return () => h('input', { 'type': 'checkbox', 'data-label': props.label }) },
|
||||
})
|
||||
|
||||
// Placeholder (onglets Fournisseurs/Clients) : marqueur sans appel API.
|
||||
const TabsStub = defineComponent({ setup() { return () => h('div', { 'data-testid': 'placeholder-tabs' }) } })
|
||||
|
||||
const stubs = {
|
||||
MalioButtonIcon: ButtonStub,
|
||||
MalioButton: ButtonStub,
|
||||
@@ -74,6 +77,7 @@ const stubs = {
|
||||
MalioSelect: InputStub,
|
||||
MalioSelectCheckbox: InputStub,
|
||||
MalioCheckbox: CheckboxStub,
|
||||
ProductPlaceholderTabs: TabsStub,
|
||||
}
|
||||
|
||||
async function mountPage() {
|
||||
|
||||
Reference in New Issue
Block a user