Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #86.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { ApiProvider, createApiClient, createQueryClient, createVerifiedSession } from '../api/index.ts'
|
||||
import { createNetworkStatus } from '../pwa/networkStatus.ts'
|
||||
import { errorResponse, fakeLocation, jsonResponse } from './fetchStub.ts'
|
||||
|
||||
// AI_SETTINGS is a GET /api/v1/settings/ai body: Anthropic with a verified key.
|
||||
export const AI_SETTINGS = {
|
||||
provider: 'anthropic',
|
||||
base_url: '',
|
||||
model: 'claude-sonnet-4-5',
|
||||
temperature: 0.7,
|
||||
system_prompt: 'You are MealQuest, a dinner planning assistant for one household.',
|
||||
api_key_last4: '3f9a',
|
||||
key_verified_at: '2026-10-01T15:00:00.000Z',
|
||||
updated_at: '2026-10-01T15:00:00.000Z',
|
||||
}
|
||||
|
||||
// AI_MODELS is a GET /api/v1/settings/ai/models body: the model AI_SETTINGS
|
||||
// chooses.
|
||||
export const AI_MODELS = { models: [{ id: 'claude-sonnet-4-5', display_name: 'Claude Sonnet' }] }
|
||||
|
||||
// HOUSEHOLD_SETTINGS is a GET /api/v1/settings/household body: the seed
|
||||
// times in New York.
|
||||
export const HOUSEHOLD_SETTINGS = {
|
||||
timezone: 'America/New_York',
|
||||
dinner_time: '17:30',
|
||||
reminder_time: '16:30',
|
||||
review_time: '09:00',
|
||||
plan_draft_dow: 5,
|
||||
plan_draft_time: '09:00',
|
||||
suggestions_dow: 0,
|
||||
suggestions_time: '08:00',
|
||||
chat_autosend_dow: 6,
|
||||
chat_autosend_time: '08:00',
|
||||
onboarding_completed_at: null,
|
||||
updates_seen_at: null,
|
||||
updated_by: null,
|
||||
created_at: '2026-10-01T15:00:00.000Z',
|
||||
updated_at: '2026-10-01T15:00:00.000Z',
|
||||
}
|
||||
|
||||
// RECIPE_SEARCH_SETTINGS is a GET /api/v1/settings/recipe-search body: the
|
||||
// SearXNG sidecar, no Brave key stored.
|
||||
export const RECIPE_SEARCH_SETTINGS = {
|
||||
provider: 'searxng',
|
||||
base_url: 'http://searxng:8080',
|
||||
default_base_url: 'http://searxng:8080',
|
||||
api_key_set: false,
|
||||
api_key_last4: null,
|
||||
updated_at: '2026-10-01T15:00:00.000Z',
|
||||
}
|
||||
|
||||
// SETTINGS_API answers the settings home's requests, for stubFetchByPath, as
|
||||
// today's server does: the dinners, Google Calendar and notifications status
|
||||
// endpoints do not exist yet. The home's sub-tabs stay mounted on mobile, so
|
||||
// it also reads the AI models and recipe search settings.
|
||||
export const SETTINGS_API = {
|
||||
'/api/v1/dinners?status=regular': () => errorResponse(404, 'not_found'),
|
||||
'/api/v1/settings/calendar': () => errorResponse(404, 'not_found'),
|
||||
'/api/v1/settings/notifications': () => errorResponse(404, 'not_found'),
|
||||
'/api/v1/settings/household': () => jsonResponse(200, HOUSEHOLD_SETTINGS),
|
||||
'/api/v1/settings/ai': () => jsonResponse(200, AI_SETTINGS),
|
||||
'/api/v1/settings/ai/models': () => jsonResponse(200, AI_MODELS),
|
||||
'/api/v1/settings/recipe-search': () => jsonResponse(200, RECIPE_SEARCH_SETTINGS),
|
||||
}
|
||||
|
||||
// ApiProviders gives the pages under test what App gives them: an API client
|
||||
// on https://mealquest.test and a fresh query client.
|
||||
export function ApiProviders({ children }: { children: ReactNode }) {
|
||||
const [api] = useState(() =>
|
||||
createApiClient(fakeLocation(), createNetworkStatus(window), createVerifiedSession(localStorage)),
|
||||
)
|
||||
const [queryClient] = useState(createQueryClient)
|
||||
return (
|
||||
<ApiProvider client={api}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</ApiProvider>
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,13 @@ function cssRules(): CSSRule[] {
|
||||
return Array.from(document.styleSheets).flatMap((sheet) => Array.from(sheet.cssRules))
|
||||
}
|
||||
|
||||
// A selector with a pseudo-element never matches an element, and jsdom's
|
||||
// matches() throws on the vendor ones (::-moz-range-track), so those rules
|
||||
// are skipped.
|
||||
function collect(element: Element, rules: CSSRule[], style: CSSStyleDeclaration) {
|
||||
for (const rule of rules) {
|
||||
if (!(rule instanceof CSSStyleRule) || !element.matches(rule.selectorText)) continue
|
||||
if (!(rule instanceof CSSStyleRule) || rule.selectorText.includes('::')) continue
|
||||
if (!element.matches(rule.selectorText)) continue
|
||||
for (const property of Array.from(rule.style)) {
|
||||
style.setProperty(property, rule.style.getPropertyValue(property))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router'
|
||||
|
||||
// InDataRouter renders children under a memory data router's splat route at
|
||||
// path, as main.tsx renders App, so pages that block navigation (useBlocker)
|
||||
// work in tests. The router is created once, on first render.
|
||||
export function InDataRouter({ path, children }: { path: string; children: ReactNode }) {
|
||||
const [router] = useState(() => createMemoryRouter([{ path: '*', element: children }], { initialEntries: [path] }))
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
@@ -47,3 +47,17 @@ export function request(fetchMock: Mock<typeof fetch>, n: number) {
|
||||
export function fakeLocation(pathname = '/', search = ''): ApiLocation & { assign: Mock<(url: string | URL) => void> } {
|
||||
return { origin: 'https://mealquest.test', pathname, search, assign: vi.fn<(url: string | URL) => void>() }
|
||||
}
|
||||
|
||||
// stubFetchByPath replaces the global fetch with a mock that answers each
|
||||
// path from responses with a fresh response from its factory, so requests
|
||||
// sent in parallel need no order; a path not in responses fails the test.
|
||||
// Undo with vi.unstubAllGlobals.
|
||||
export function stubFetchByPath(responses: Record<string, () => Response>): Mock<typeof fetch> {
|
||||
const fetchMock = vi.fn<typeof fetch>((input, init) => {
|
||||
const respond = Object.hasOwn(responses, String(input)) ? responses[String(input)] : undefined
|
||||
if (respond === undefined) throw new Error(`unexpected fetch: ${init?.method ?? 'GET'} ${String(input)}`)
|
||||
return Promise.resolve(respond())
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
return fetchMock
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user