test: integration coverage for stores, services, MockAIService, and hooks
- Stores (29 tests): compareStore max-4 enforcement, toastStore unique IDs, shortlistStore dialog state, assistantStore context merge + clearConversation - Services (37 tests): needService CRUD, propertyService dashboard summary active-status filter, matchService computeMatchesForNeed + getStrongMatches, futureSignalService summary math (distribution totals, highConfidence count) - MockAIService (18 tests): follow-up question priority ordering, area ambiguity detection (ratio >8 and zero values), max-3 cap, trade-off risk logic (LOW/MEDIUM/HIGH thresholds), match explanation headline tiers, provenance shape - Hooks (20 tests): useNeeds/useProperties/useMatches envelope unwrap, disabled- when-empty guards, useCreateNeed/useCreateProperty mutations, useNeed/useMatchDetail All 257 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createElement } from 'react'
|
||||
import { useMatches, useMatchesByNeed, useMatchesByProperty, useMatchDetail } from '../useMatches'
|
||||
|
||||
function wrapper() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: qc }, children)
|
||||
}
|
||||
|
||||
describe('useMatches', () => {
|
||||
it('returns an array of matches', async () => {
|
||||
const { result } = renderHook(() => useMatches(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(Array.isArray(result.current.data)).toBe(true)
|
||||
expect((result.current.data ?? []).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('select unwraps the envelope — data is Match[]', async () => {
|
||||
const { result } = renderHook(() => useMatches(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
const first = result.current.data?.[0]
|
||||
expect(typeof first?.id).toBe('string')
|
||||
expect(typeof first?.matchScore).toBe('number')
|
||||
expect(typeof first?.needId).toBe('string')
|
||||
expect(typeof first?.propertyId).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMatchesByNeed', () => {
|
||||
it('returns matches for the given needId', async () => {
|
||||
const { result: all } = renderHook(() => useMatches(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const needId = all.current.data![0].needId
|
||||
|
||||
const { result } = renderHook(() => useMatchesByNeed(needId), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.every(m => m.needId === needId)).toBe(true)
|
||||
})
|
||||
|
||||
it('is disabled when needId is empty string', () => {
|
||||
const { result } = renderHook(() => useMatchesByNeed(''), { wrapper: wrapper() })
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMatchesByProperty', () => {
|
||||
it('returns matches for the given propertyId', async () => {
|
||||
const { result: all } = renderHook(() => useMatches(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const propertyId = all.current.data![0].propertyId
|
||||
|
||||
const { result } = renderHook(() => useMatchesByProperty(propertyId), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.every(m => m.propertyId === propertyId)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMatchDetail', () => {
|
||||
it('returns a single match for a valid id', async () => {
|
||||
const { result: all } = renderHook(() => useMatches(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const id = all.current.data![0].id
|
||||
|
||||
const { result } = renderHook(() => useMatchDetail(id), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.id).toBe(id)
|
||||
})
|
||||
|
||||
it('is disabled when id is empty string', () => {
|
||||
const { result } = renderHook(() => useMatchDetail(''), { wrapper: wrapper() })
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createElement } from 'react'
|
||||
import { useNeeds, useNeed, useCreateNeed } from '../useNeeds'
|
||||
|
||||
function wrapper() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: qc }, children)
|
||||
}
|
||||
|
||||
describe('useNeeds', () => {
|
||||
it('returns an array of needs when data loads', async () => {
|
||||
const { result } = renderHook(() => useNeeds(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(Array.isArray(result.current.data)).toBe(true)
|
||||
expect((result.current.data ?? []).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('select unwraps the data envelope so data is Need[]', async () => {
|
||||
const { result } = renderHook(() => useNeeds(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
const first = result.current.data?.[0]
|
||||
expect(typeof first?.id).toBe('string')
|
||||
expect(typeof first?.companyName).toBe('string')
|
||||
})
|
||||
|
||||
it('accepts refetchOnMount option without changing query key', async () => {
|
||||
const { result } = renderHook(() => useNeeds({ refetchOnMount: 'always' }), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useNeed', () => {
|
||||
it('returns a single need for a valid id', async () => {
|
||||
// First fetch all to get a real id
|
||||
const { result: all } = renderHook(() => useNeeds(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const id = all.current.data![0].id
|
||||
|
||||
const { result } = renderHook(() => useNeed(id), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.id).toBe(id)
|
||||
})
|
||||
|
||||
it('is disabled when id is empty string', () => {
|
||||
const { result } = renderHook(() => useNeed(''), { wrapper: wrapper() })
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useCreateNeed', () => {
|
||||
it('mutates successfully and returns the created need', async () => {
|
||||
const { result } = renderHook(() => useCreateNeed(), { wrapper: wrapper() })
|
||||
result.current.mutate({
|
||||
companyName: 'Hook Test GmbH',
|
||||
assetType: 'OFFICE',
|
||||
requiredArea: { min: 200, max: 400 },
|
||||
preferredLocations: ['Zürich'],
|
||||
budgetRange: { maxPerSqm: 400, maxMonthlyTotal: 10000, currency: 'CHF' },
|
||||
organizationId: 'org-test',
|
||||
})
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.data.companyName).toBe('Hook Test GmbH')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createElement } from 'react'
|
||||
import { useProperties, useProperty, usePropertyById, useCreateProperty } from '../useProperties'
|
||||
|
||||
function wrapper() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: qc }, children)
|
||||
}
|
||||
|
||||
describe('useProperties', () => {
|
||||
it('returns a non-empty array of properties', async () => {
|
||||
const { result } = renderHook(() => useProperties(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(Array.isArray(result.current.data)).toBe(true)
|
||||
expect((result.current.data ?? []).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('select unwraps the envelope — data is Property[]', async () => {
|
||||
const { result } = renderHook(() => useProperties(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
const first = result.current.data?.[0]
|
||||
expect(typeof first?.id).toBe('string')
|
||||
expect(typeof first?.areaSqm).toBe('number')
|
||||
})
|
||||
|
||||
it('accepts an assetType filter', async () => {
|
||||
const { result } = renderHook(() => useProperties({ assetType: 'OFFICE' }), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
// Filter may or may not be implemented in mock provider — just verify it doesn't error
|
||||
expect(result.current.isError).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useProperty', () => {
|
||||
it('returns the property for a valid id', async () => {
|
||||
const { result: all } = renderHook(() => useProperties(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const id = all.current.data![0].id
|
||||
|
||||
const { result } = renderHook(() => useProperty(id), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.id).toBe(id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePropertyById', () => {
|
||||
it('is disabled when id is null', () => {
|
||||
const { result } = renderHook(() => usePropertyById(null), { wrapper: wrapper() })
|
||||
expect(result.current.fetchStatus).toBe('idle')
|
||||
})
|
||||
|
||||
it('fetches property when id is provided', async () => {
|
||||
const { result: all } = renderHook(() => useProperties(), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(all.current.isSuccess).toBe(true))
|
||||
const id = all.current.data![0].id
|
||||
|
||||
const { result } = renderHook(() => usePropertyById(id), { wrapper: wrapper() })
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.id).toBe(id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useCreateProperty', () => {
|
||||
it('creates a property and returns it', async () => {
|
||||
const { result } = renderHook(() => useCreateProperty(), { wrapper: wrapper() })
|
||||
result.current.mutate({
|
||||
title: 'Hook-Test Objekt',
|
||||
assetType: 'LOGISTICS',
|
||||
resultType: 'VERIFIED_PORTFOLIO',
|
||||
location: { city: 'Bern', country: 'CH' },
|
||||
areaSqm: 300,
|
||||
rentPricePerSqm: 100,
|
||||
availabilityStatus: 'AVAILABLE_NOW',
|
||||
organizationId: 'org-test',
|
||||
})
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.data.title).toBe('Hook-Test Objekt')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user