test: configure Vitest and add 54 unit tests (F6.1, F6.2)

Set up Vitest with happy-dom, mock Nuxt auto-imports via #imports alias.
Add tests for: inventory-types validators (9), apiHelpers (10),
modelUtils (18), useConfirm (8), useToast (9). All 54 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-09 11:20:28 +01:00
parent 6152848957
commit 634184c2be
9 changed files with 1099 additions and 11 deletions

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest'
import { extractCollection } from '~/shared/utils/apiHelpers'
describe('extractCollection', () => {
it('returns the input if it is already an array', () => {
const items = [{ id: 1 }, { id: 2 }]
expect(extractCollection(items)).toEqual(items)
})
it('extracts from hydra:member', () => {
const payload = { 'hydra:member': [{ id: 1 }], 'hydra:totalItems': 1 }
expect(extractCollection(payload)).toEqual([{ id: 1 }])
})
it('extracts from member', () => {
const payload = { member: [{ id: 1 }, { id: 2 }] }
expect(extractCollection(payload)).toEqual([{ id: 1 }, { id: 2 }])
})
it('extracts from items', () => {
const payload = { items: [{ id: 1 }] }
expect(extractCollection(payload)).toEqual([{ id: 1 }])
})
it('extracts from data', () => {
const payload = { data: [{ id: 1 }] }
expect(extractCollection(payload)).toEqual([{ id: 1 }])
})
it('prefers member over hydra:member', () => {
const payload = { member: [{ id: 'member' }], 'hydra:member': [{ id: 'hydra' }] }
expect(extractCollection(payload)).toEqual([{ id: 'member' }])
})
it('returns empty array for null', () => {
expect(extractCollection(null)).toEqual([])
})
it('returns empty array for undefined', () => {
expect(extractCollection(undefined)).toEqual([])
})
it('returns empty array for empty object', () => {
expect(extractCollection({})).toEqual([])
})
it('returns empty array for string', () => {
expect(extractCollection('not an object')).toEqual([])
})
})

View File

@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest'
import {
componentModelStructureValidator,
createEmptyComponentModelStructure,
createEmptyPieceModelStructure,
createEmptyProductModelStructure,
} from '~/shared/types/inventory'
describe('createEmptyComponentModelStructure', () => {
it('returns a valid empty structure', () => {
const result = createEmptyComponentModelStructure()
expect(result).toEqual({
customFields: [],
pieces: [],
products: [],
subcomponents: [],
})
})
})
describe('createEmptyPieceModelStructure', () => {
it('returns a valid empty piece structure', () => {
const result = createEmptyPieceModelStructure()
expect(result).toEqual({
customFields: [],
products: [],
})
})
})
describe('createEmptyProductModelStructure', () => {
it('returns a valid empty product structure', () => {
const result = createEmptyProductModelStructure()
expect(result).toEqual({
customFields: [],
})
})
})
describe('componentModelStructureValidator', () => {
it('parses a minimal valid structure', () => {
const input = {
customFields: [],
pieces: [],
subcomponents: [],
}
const result = componentModelStructureValidator.safeParse(input)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.customFields).toEqual([])
expect(result.data.pieces).toEqual([])
expect(result.data.subcomponents).toEqual([])
}
})
it('rejects a non-object input', () => {
const result = componentModelStructureValidator.safeParse('not an object')
expect(result.success).toBe(false)
if (!result.success) {
expect(result.issues.length).toBeGreaterThan(0)
}
})
it('validates custom fields with name and type', () => {
const input = {
customFields: [
{ name: 'Color', type: 'text', required: true },
{ name: 'Size', type: 'select', required: false, options: ['S', 'M', 'L'] },
],
pieces: [],
subcomponents: [],
}
const result = componentModelStructureValidator.safeParse(input)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.customFields).toHaveLength(2)
expect(result.data.customFields[0].name).toBe('Color')
expect(result.data.customFields[1].options).toEqual(['S', 'M', 'L'])
}
})
it('rejects custom fields without name', () => {
const input = {
customFields: [{ type: 'text', required: false }],
pieces: [],
subcomponents: [],
}
const result = componentModelStructureValidator.safeParse(input)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.issues.some((i) => i.includes('name'))).toBe(true)
}
})
it('validates nested subcomponents', () => {
const input = {
customFields: [],
pieces: [],
subcomponents: [
{
typeComposantId: 'abc-123',
alias: 'Motor',
subcomponents: [],
},
],
}
const result = componentModelStructureValidator.safeParse(input)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.subcomponents).toHaveLength(1)
expect(result.data.subcomponents[0].alias).toBe('Motor')
}
})
it('parse throws on invalid input', () => {
expect(() => componentModelStructureValidator.parse(null)).toThrow()
})
})

View File

@@ -0,0 +1,169 @@
import { describe, it, expect } from 'vitest'
import {
isPlainObject,
defaultStructure,
cloneStructure,
computeStructureStats,
formatStructurePreview,
} from '~/shared/model/componentStructure'
import {
defaultPieceStructure,
defaultProductStructure,
clonePieceStructure,
cloneProductStructure,
} from '~/shared/model/pieceProductStructure'
describe('isPlainObject', () => {
it('returns true for plain objects', () => {
expect(isPlainObject({})).toBe(true)
expect(isPlainObject({ key: 'value' })).toBe(true)
})
it('returns false for arrays', () => {
expect(isPlainObject([])).toBe(false)
})
it('returns false for null', () => {
expect(isPlainObject(null)).toBe(false)
})
it('returns false for primitives', () => {
expect(isPlainObject('string')).toBe(false)
expect(isPlainObject(42)).toBe(false)
expect(isPlainObject(undefined)).toBe(false)
})
})
describe('defaultStructure', () => {
it('returns a fresh empty structure each time', () => {
const a = defaultStructure()
const b = defaultStructure()
expect(a).toEqual(b)
expect(a).not.toBe(b)
expect(a.customFields).toEqual([])
expect(a.pieces).toEqual([])
expect(a.products).toEqual([])
expect(a.subcomponents).toEqual([])
})
})
describe('cloneStructure', () => {
it('deep clones a structure', () => {
const original = defaultStructure()
original.customFields.push({ name: 'test', type: 'text', required: false })
const cloned = cloneStructure(original)
expect(cloned.customFields).toHaveLength(1)
expect(cloned.customFields[0].name).toBe('test')
// Ensure deep clone — mutating original doesn't affect clone
original.customFields[0].name = 'mutated'
expect(cloned.customFields[0].name).toBe('test')
})
it('returns default structure for null input', () => {
const result = cloneStructure(null)
expect(result).toEqual(defaultStructure())
})
it('returns default structure for undefined input', () => {
const result = cloneStructure(undefined)
expect(result).toEqual(defaultStructure())
})
it('preserves typeComposantId and alias', () => {
const input = {
...defaultStructure(),
typeComposantId: 'abc-123',
alias: 'Motor',
}
const result = cloneStructure(input)
expect(result.typeComposantId).toBe('abc-123')
expect(result.alias).toBe('Motor')
})
})
describe('computeStructureStats', () => {
it('counts elements in a structure', () => {
const structure = {
...defaultStructure(),
customFields: [
{ name: 'A', type: 'text' as const, required: false },
{ name: 'B', type: 'number' as const, required: true },
],
pieces: [{ typePieceId: 'p1' }],
products: [{ typeProductId: 'pr1' }],
subcomponents: [{ subcomponents: [] }],
}
const stats = computeStructureStats(structure)
expect(stats.customFields).toBe(2)
expect(stats.pieces).toBe(1)
expect(stats.products).toBe(1)
expect(stats.subcomponents).toBe(1)
})
it('returns zeros for empty structure', () => {
const stats = computeStructureStats(defaultStructure())
expect(stats).toEqual({
customFields: 0,
pieces: 0,
products: 0,
subcomponents: 0,
})
})
})
describe('formatStructurePreview', () => {
it('returns "Structure vide" for empty structure', () => {
const result = formatStructurePreview(defaultStructure())
expect(result).toBe('Structure vide')
})
it('formats a non-empty structure', () => {
const structure = {
...defaultStructure(),
customFields: [{ name: 'A', type: 'text' as const, required: false }],
pieces: [{ typePieceId: 'p1' }, { typePieceId: 'p2' }],
}
const result = formatStructurePreview(structure)
expect(result).toContain('1 champ')
expect(result).toContain('2 pièce')
})
})
describe('defaultPieceStructure', () => {
it('returns a valid empty piece structure', () => {
const result = defaultPieceStructure()
expect(result.customFields).toEqual([])
expect(result.products).toEqual([])
})
})
describe('defaultProductStructure', () => {
it('returns a valid empty product structure', () => {
const result = defaultProductStructure()
expect(result.customFields).toEqual([])
})
})
describe('clonePieceStructure', () => {
it('deep clones a piece structure', () => {
const original = defaultPieceStructure()
original.customFields.push({ name: 'Weight', type: 'number', required: true })
const cloned = clonePieceStructure(original)
expect(cloned.customFields).toHaveLength(1)
original.customFields[0].name = 'mutated'
expect(cloned.customFields[0].name).toBe('Weight')
})
it('returns default for null', () => {
expect(clonePieceStructure(null)).toEqual(defaultPieceStructure())
})
})
describe('cloneProductStructure', () => {
it('deep clones a product structure', () => {
const original = defaultProductStructure()
original.customFields.push({ name: 'Color', type: 'text', required: false })
const cloned = cloneProductStructure(original)
expect(cloned.customFields).toHaveLength(1)
})
})