feat: add API optimizations, cache invalidation and comprehensive test suite
- Add abort controllers and request deduplication to composables - Add entity type cache invalidation on create/update/delete flows - Add 179 new tests (utilities, services, composables, components) - Fix Vue runtime warnings in structure editors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
228
tests/services/modelTypes.test.ts
Normal file
228
tests/services/modelTypes.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Import AFTER mocking
|
||||
import {
|
||||
createModelType,
|
||||
updateModelType,
|
||||
deleteModelType,
|
||||
getModelType,
|
||||
type ModelType,
|
||||
type ModelTypePayload,
|
||||
} from '~/services/modelTypes'
|
||||
|
||||
// The service uses useRequestFetch from #imports (explicit import)
|
||||
// AND useRuntimeConfig as a Nuxt auto-import (bare global).
|
||||
const mockFetch = vi.fn()
|
||||
|
||||
// Mock the explicit #imports module
|
||||
vi.mock('#imports', () => ({
|
||||
useRuntimeConfig: () => ({
|
||||
public: { apiBaseUrl: 'http://test-api:8081/api' },
|
||||
}),
|
||||
useRequestFetch: () => mockFetch,
|
||||
}))
|
||||
|
||||
// Also stub the global auto-import (Nuxt makes these globally available)
|
||||
vi.stubGlobal('useRuntimeConfig', () => ({
|
||||
public: { apiBaseUrl: 'http://test-api:8081/api' },
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const fakeModelType = (overrides: Partial<ModelType> = {}): ModelType => ({
|
||||
id: 'mt-1',
|
||||
name: 'Test Type',
|
||||
code: 'test-type',
|
||||
category: 'COMPONENT',
|
||||
structure: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
updatedAt: '2025-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// normalizeModelType (tested via getModelType which calls .then(normalizeModelType))
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('normalizeModelType (via getModelType)', () => {
|
||||
it('maps componentSkeleton to structure for COMPONENT', async () => {
|
||||
const skeleton = { customFields: [{ name: 'Weight' }] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({
|
||||
category: 'COMPONENT',
|
||||
structure: null,
|
||||
componentSkeleton: skeleton as any,
|
||||
}))
|
||||
|
||||
const result = await getModelType('mt-1')
|
||||
expect(result.structure).toEqual(skeleton)
|
||||
})
|
||||
|
||||
it('maps pieceSkeleton to structure for PIECE', async () => {
|
||||
const skeleton = { customFields: [{ name: 'Size' }] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({
|
||||
category: 'PIECE',
|
||||
structure: null,
|
||||
pieceSkeleton: skeleton as any,
|
||||
}))
|
||||
|
||||
const result = await getModelType('mt-1')
|
||||
expect(result.structure).toEqual(skeleton)
|
||||
})
|
||||
|
||||
it('maps productSkeleton to structure for PRODUCT', async () => {
|
||||
const skeleton = { customFields: [{ name: 'Brand' }] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({
|
||||
category: 'PRODUCT',
|
||||
structure: null,
|
||||
productSkeleton: skeleton as any,
|
||||
}))
|
||||
|
||||
const result = await getModelType('mt-1')
|
||||
expect(result.structure).toEqual(skeleton)
|
||||
})
|
||||
|
||||
it('does not override existing structure', async () => {
|
||||
const existing = { customFields: [{ name: 'Existing' }] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({
|
||||
category: 'COMPONENT',
|
||||
structure: existing as any,
|
||||
componentSkeleton: { customFields: [{ name: 'Skeleton' }] } as any,
|
||||
}))
|
||||
|
||||
const result = await getModelType('mt-1')
|
||||
expect(result.structure).toEqual(existing)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createModelType — maps structure to skeleton
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('createModelType', () => {
|
||||
it('sends POST with componentSkeleton for COMPONENT', async () => {
|
||||
const structure = { customFields: [] }
|
||||
mockFetch.mockResolvedValue(fakeModelType())
|
||||
|
||||
const payload: ModelTypePayload = {
|
||||
name: 'New Type',
|
||||
code: 'new-type',
|
||||
category: 'COMPONENT',
|
||||
structure: structure as any,
|
||||
}
|
||||
|
||||
await createModelType(payload)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce()
|
||||
const [endpoint, options] = mockFetch.mock.calls[0]
|
||||
expect(endpoint).toBe('/model_types')
|
||||
expect(options.method).toBe('POST')
|
||||
expect(options.body.componentSkeleton).toEqual(structure)
|
||||
expect(options.body.structure).toBeUndefined()
|
||||
})
|
||||
|
||||
it('sends POST with pieceSkeleton for PIECE', async () => {
|
||||
const structure = { customFields: [], products: [] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({ category: 'PIECE' }))
|
||||
|
||||
await createModelType({
|
||||
name: 'Piece Type',
|
||||
code: 'piece-type',
|
||||
category: 'PIECE',
|
||||
structure: structure as any,
|
||||
})
|
||||
|
||||
const [, options] = mockFetch.mock.calls[0]
|
||||
expect(options.body.pieceSkeleton).toEqual(structure)
|
||||
expect(options.body.structure).toBeUndefined()
|
||||
})
|
||||
|
||||
it('sends POST with productSkeleton for PRODUCT', async () => {
|
||||
const structure = { customFields: [] }
|
||||
mockFetch.mockResolvedValue(fakeModelType({ category: 'PRODUCT' }))
|
||||
|
||||
await createModelType({
|
||||
name: 'Product Type',
|
||||
code: 'product-type',
|
||||
category: 'PRODUCT',
|
||||
structure: structure as any,
|
||||
})
|
||||
|
||||
const [, options] = mockFetch.mock.calls[0]
|
||||
expect(options.body.productSkeleton).toEqual(structure)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateModelType — maps structure to skeleton
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('updateModelType', () => {
|
||||
it('sends PATCH with correct endpoint and skeleton', async () => {
|
||||
const structure = { customFields: [{ name: 'Updated' }] }
|
||||
mockFetch.mockResolvedValue(fakeModelType())
|
||||
|
||||
await updateModelType('mt-1', {
|
||||
name: 'Updated',
|
||||
code: 'updated',
|
||||
category: 'COMPONENT',
|
||||
structure: structure as any,
|
||||
})
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce()
|
||||
const [endpoint, options] = mockFetch.mock.calls[0]
|
||||
expect(endpoint).toBe('/model_types/mt-1')
|
||||
expect(options.method).toBe('PATCH')
|
||||
expect(options.headers['Content-Type']).toBe('application/merge-patch+json')
|
||||
expect(options.body.componentSkeleton).toEqual(structure)
|
||||
})
|
||||
|
||||
it('sends payload without skeleton when no structure', async () => {
|
||||
mockFetch.mockResolvedValue(fakeModelType())
|
||||
|
||||
await updateModelType('mt-1', {
|
||||
name: 'Just Name',
|
||||
code: 'just-name',
|
||||
category: 'COMPONENT',
|
||||
})
|
||||
|
||||
const [, options] = mockFetch.mock.calls[0]
|
||||
expect(options.body.componentSkeleton).toBeUndefined()
|
||||
expect(options.body.name).toBe('Just Name')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deleteModelType
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('deleteModelType', () => {
|
||||
it('sends DELETE to correct endpoint', async () => {
|
||||
mockFetch.mockResolvedValue(undefined)
|
||||
|
||||
await deleteModelType('mt-42')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce()
|
||||
const [endpoint, options] = mockFetch.mock.calls[0]
|
||||
expect(endpoint).toBe('/model_types/mt-42')
|
||||
expect(options.method).toBe('DELETE')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getModelType
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getModelType', () => {
|
||||
it('sends GET to correct endpoint', async () => {
|
||||
mockFetch.mockResolvedValue(fakeModelType({ id: 'mt-99' }))
|
||||
|
||||
const result = await getModelType('mt-99')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce()
|
||||
const [endpoint, options] = mockFetch.mock.calls[0]
|
||||
expect(endpoint).toBe('/model_types/mt-99')
|
||||
expect(options.method).toBe('GET')
|
||||
expect(result.id).toBe('mt-99')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user