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:
35
tests/__mocks__/imports.ts
Normal file
35
tests/__mocks__/imports.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Minimal mock for Nuxt's #imports auto-import.
|
||||
* Add stubs here as tests require them.
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useRuntimeConfig = () => ({
|
||||
public: {
|
||||
apiBaseUrl: 'http://localhost:8081/api',
|
||||
appVersion: '0.0.0-test',
|
||||
},
|
||||
})
|
||||
|
||||
export const useRoute = () => ({
|
||||
path: '/',
|
||||
params: {},
|
||||
query: {},
|
||||
})
|
||||
|
||||
export const useRouter = () => ({
|
||||
push: () => Promise.resolve(),
|
||||
replace: () => Promise.resolve(),
|
||||
})
|
||||
|
||||
export const navigateTo = () => Promise.resolve()
|
||||
|
||||
export const useRequestFetch = () => fetch
|
||||
|
||||
export const useFetch = () => ({
|
||||
data: ref(null),
|
||||
error: ref(null),
|
||||
pending: ref(false),
|
||||
refresh: () => Promise.resolve(),
|
||||
})
|
||||
81
tests/composables/useConfirm.test.ts
Normal file
81
tests/composables/useConfirm.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { useConfirm } from '~/composables/useConfirm'
|
||||
|
||||
describe('useConfirm', () => {
|
||||
it('returns confirm function and state', () => {
|
||||
const { confirm, confirmState, handleConfirm, handleCancel } = useConfirm()
|
||||
expect(typeof confirm).toBe('function')
|
||||
expect(typeof handleConfirm).toBe('function')
|
||||
expect(typeof handleCancel).toBe('function')
|
||||
expect(confirmState.open).toBe(false)
|
||||
})
|
||||
|
||||
it('opens modal with correct options', () => {
|
||||
const { confirm, confirmState } = useConfirm()
|
||||
// Don't await — we'll manually resolve
|
||||
confirm({ message: 'Delete this item?' })
|
||||
expect(confirmState.open).toBe(true)
|
||||
expect(confirmState.message).toBe('Delete this item?')
|
||||
expect(confirmState.title).toBe('Confirmation')
|
||||
expect(confirmState.confirmText).toBe('Supprimer')
|
||||
expect(confirmState.cancelText).toBe('Annuler')
|
||||
expect(confirmState.dangerous).toBe(true)
|
||||
// Clean up by canceling
|
||||
const { handleCancel } = useConfirm()
|
||||
handleCancel()
|
||||
})
|
||||
|
||||
it('resolves true on confirm', async () => {
|
||||
const { confirm, handleConfirm } = useConfirm()
|
||||
const promise = confirm({ message: 'Confirm?' })
|
||||
handleConfirm()
|
||||
const result = await promise
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves false on cancel', async () => {
|
||||
const { confirm, handleCancel } = useConfirm()
|
||||
const promise = confirm({ message: 'Cancel?' })
|
||||
handleCancel()
|
||||
const result = await promise
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('closes modal after confirm', async () => {
|
||||
const { confirm, confirmState, handleConfirm } = useConfirm()
|
||||
confirm({ message: 'Test' })
|
||||
expect(confirmState.open).toBe(true)
|
||||
handleConfirm()
|
||||
expect(confirmState.open).toBe(false)
|
||||
})
|
||||
|
||||
it('closes modal after cancel', async () => {
|
||||
const { confirm, confirmState, handleCancel } = useConfirm()
|
||||
confirm({ message: 'Test' })
|
||||
expect(confirmState.open).toBe(true)
|
||||
handleCancel()
|
||||
expect(confirmState.open).toBe(false)
|
||||
})
|
||||
|
||||
it('supports custom options', () => {
|
||||
const { confirm, confirmState, handleCancel } = useConfirm()
|
||||
confirm({
|
||||
title: 'Custom Title',
|
||||
message: 'Custom message',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'No',
|
||||
dangerous: false,
|
||||
})
|
||||
expect(confirmState.title).toBe('Custom Title')
|
||||
expect(confirmState.confirmText).toBe('Yes')
|
||||
expect(confirmState.cancelText).toBe('No')
|
||||
expect(confirmState.dangerous).toBe(false)
|
||||
handleCancel()
|
||||
})
|
||||
|
||||
it('shares state across calls (singleton)', () => {
|
||||
const a = useConfirm()
|
||||
const b = useConfirm()
|
||||
expect(a.confirmState).toBe(b.confirmState)
|
||||
})
|
||||
})
|
||||
83
tests/composables/useToast.test.ts
Normal file
83
tests/composables/useToast.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
describe('useToast', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
const { clearAll } = useToast()
|
||||
clearAll()
|
||||
})
|
||||
|
||||
it('returns all expected functions', () => {
|
||||
const toast = useToast()
|
||||
expect(typeof toast.showToast).toBe('function')
|
||||
expect(typeof toast.showSuccess).toBe('function')
|
||||
expect(typeof toast.showError).toBe('function')
|
||||
expect(typeof toast.showWarning).toBe('function')
|
||||
expect(typeof toast.showInfo).toBe('function')
|
||||
expect(typeof toast.removeToast).toBe('function')
|
||||
expect(typeof toast.clearAll).toBe('function')
|
||||
})
|
||||
|
||||
it('adds a toast with correct properties', () => {
|
||||
const { showToast, toasts } = useToast()
|
||||
const id = showToast('Hello', 'info')
|
||||
expect(toasts.value).toHaveLength(1)
|
||||
expect(toasts.value[0].message).toBe('Hello')
|
||||
expect(toasts.value[0].type).toBe('info')
|
||||
expect(toasts.value[0].visible).toBe(true)
|
||||
expect(toasts.value[0].id).toBe(id)
|
||||
})
|
||||
|
||||
it('showSuccess creates a success toast', () => {
|
||||
const { showSuccess, toasts } = useToast()
|
||||
showSuccess('Saved!')
|
||||
expect(toasts.value[0].type).toBe('success')
|
||||
expect(toasts.value[0].message).toBe('Saved!')
|
||||
})
|
||||
|
||||
it('showError creates an error toast', () => {
|
||||
const { showError, toasts } = useToast()
|
||||
showError('Failed!')
|
||||
expect(toasts.value[0].type).toBe('error')
|
||||
})
|
||||
|
||||
it('showWarning creates a warning toast', () => {
|
||||
const { showWarning, toasts } = useToast()
|
||||
showWarning('Caution!')
|
||||
expect(toasts.value[0].type).toBe('warning')
|
||||
})
|
||||
|
||||
it('limits to MAX_TOASTS (3)', () => {
|
||||
const { showToast, toasts } = useToast()
|
||||
showToast('A', 'info')
|
||||
showToast('B', 'info')
|
||||
showToast('C', 'info')
|
||||
showToast('D', 'info')
|
||||
expect(toasts.value).toHaveLength(3)
|
||||
expect(toasts.value[0].message).toBe('B')
|
||||
expect(toasts.value[2].message).toBe('D')
|
||||
})
|
||||
|
||||
it('clearAll removes all toasts', () => {
|
||||
const { showToast, toasts, clearAll } = useToast()
|
||||
showToast('A', 'info')
|
||||
showToast('B', 'info')
|
||||
expect(toasts.value.length).toBeGreaterThan(0)
|
||||
clearAll()
|
||||
expect(toasts.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('shares state across calls (singleton)', () => {
|
||||
const a = useToast()
|
||||
const b = useToast()
|
||||
expect(a.toasts).toBe(b.toasts)
|
||||
})
|
||||
|
||||
it('removeToast sets visible to false', () => {
|
||||
const { showToast, toasts, removeToast } = useToast()
|
||||
const id = showToast('Test', 'info')
|
||||
removeToast(id)
|
||||
expect(toasts.value[0].visible).toBe(false)
|
||||
})
|
||||
})
|
||||
50
tests/shared/apiHelpers.test.ts
Normal file
50
tests/shared/apiHelpers.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
118
tests/shared/inventory-types.test.ts
Normal file
118
tests/shared/inventory-types.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
169
tests/shared/modelUtils.test.ts
Normal file
169
tests/shared/modelUtils.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user