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:
Benjamin Sutter
2026-05-24 16:55:02 +02:00
parent e1f4beb898
commit 58a0a05eff
12 changed files with 1051 additions and 0 deletions
+76
View File
@@ -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')
})
})
+68
View File
@@ -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')
})
})
+82
View File
@@ -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')
})
})