test(desktop): follow main's store and copy changes
- local-models-surfaces: the profile mock carries the roster + key helper that settings-scope now reads - model-picker: one QueryClient per test so seeding lands on the client the picker mounts against; the jobs API answers with the seeded jobs so the on-mount refetch cannot wipe them - boot-failure-overlay: read the bridge-unavailable copy from the en dictionary - use-status-snapshot: dismiss departs through the card stack; skip motion in this file so the exit never straddles the fake/real timer swap
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
|
||||
import { MotionGlobalConfig } from 'motion/react'
|
||||
import { createElement, type ReactElement, type ReactNode } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -24,6 +25,11 @@ async function flushAsync() {
|
||||
})
|
||||
}
|
||||
|
||||
// This file is about status RPC cadence, not card choreography: the notification
|
||||
// stack's exit animation would otherwise straddle the fake→real timer swap between
|
||||
// locales and leave a departing card in the next test's DOM.
|
||||
MotionGlobalConfig.skipAnimations = true
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
||||
@@ -71,7 +77,11 @@ describe('useStatusSnapshot', () => {
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
})
|
||||
expect(screen.getByText(warning)).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: copy.notifications.dismiss }))
|
||||
// Dismiss departs through the card stack (store row on a microtask, then the exit).
|
||||
await act(async (): Promise<void> => {
|
||||
fireEvent.click(screen.getByRole('button', { name: copy.notifications.dismiss }))
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
})
|
||||
expect(screen.queryByText(warning)).toBeNull()
|
||||
await act(async (): Promise<void> => {
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
@@ -90,6 +100,9 @@ describe('useStatusSnapshot', () => {
|
||||
vi.mocked(getStatus).mockResolvedValue({ shared_profile_warning: false } as never)
|
||||
rerender({ scope: 'local-work' })
|
||||
await flushAsync()
|
||||
await act(async (): Promise<void> => {
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
})
|
||||
expect(screen.queryByText(warning)).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { en } from '@/i18n/en'
|
||||
import { $desktopBoot } from '@/store/boot'
|
||||
import { $notifications } from '@/store/notifications'
|
||||
import { $desktopOnboarding } from '@/store/onboarding'
|
||||
@@ -330,7 +331,7 @@ describe('BootFailureOverlay', () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: /repair install/i }))
|
||||
|
||||
const message = failure === 'thrown' ? 'installer permission denied'
|
||||
: failure === 'refused' ? 'bundled-immutable' : 'Desktop IPC bridge is unavailable.'
|
||||
: failure === 'refused' ? 'bundled-immutable' : en.boot.errors.ipcBridgeUnavailable
|
||||
|
||||
await waitFor(() => expect($notifications.get()).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: 'error', message })
|
||||
|
||||
@@ -13,7 +13,12 @@ import type { LocalRuntimeJob } from '@/types/hermes'
|
||||
|
||||
import { ModelPickerDialog } from './model-picker'
|
||||
|
||||
// The jobs query refetches on mount and would replace a seeded cache entry with
|
||||
// whatever the backend answers; answering with the seeded jobs keeps the two equal.
|
||||
const seededJobs: { current: readonly LocalRuntimeJob[] } = vi.hoisted(() => ({ current: [] }))
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getLocalModelsJobs: vi.fn(async () => ({ jobs: [...seededJobs.current] })),
|
||||
getLocalModelsStatus: vi.fn().mockResolvedValue({ loading: {} })
|
||||
}))
|
||||
vi.mock('@/lib/model-options', async importOriginal => ({
|
||||
@@ -60,18 +65,18 @@ const DOWNLOAD_JOB: LocalRuntimeJob = {
|
||||
error: null
|
||||
}
|
||||
|
||||
let seedClient: QueryClient | null = null
|
||||
// One client per test, created in beforeEach: the tests seed jobs BEFORE
|
||||
// rendering, so the picker must mount against the client that was seeded.
|
||||
let client: QueryClient = new QueryClient()
|
||||
|
||||
function setRuntimeJobs(jobs: readonly LocalRuntimeJob[]): void {
|
||||
// Banana's store keeps jobs in react-query, not the old atom: seed the cache
|
||||
// The jobs store keeps jobs in react-query, not an atom: seed the cache
|
||||
// directly the way production populates it (localModelsKey(owner, 'jobs')).
|
||||
seedClient?.setQueryData(localModelsKey(localModelsOwner(), 'jobs'), jobs)
|
||||
seededJobs.current = jobs
|
||||
client.setQueryData(localModelsKey(localModelsOwner(), 'jobs'), jobs)
|
||||
}
|
||||
|
||||
function renderPicker(ui?: Partial<Parameters<typeof ModelPickerDialog>[0]>) {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
seedClient = client
|
||||
|
||||
const element: ReactElement = (
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider>
|
||||
@@ -91,6 +96,7 @@ function renderPicker(ui?: Partial<Parameters<typeof ModelPickerDialog>[0]>) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
vi.mocked(requestModelOptions).mockResolvedValue(OPTIONS)
|
||||
setRuntimeJobs([])
|
||||
// These suites exercise the local-models rows, which ship behind --local.
|
||||
|
||||
@@ -15,7 +15,13 @@ vi.mock('@/hermes', async (): Promise<object> => ({
|
||||
vi.mock('@/store/profile', async (): Promise<object> => {
|
||||
const { atom } = await import('nanostores')
|
||||
|
||||
return { $activeGatewayProfile: atom<string>('work') }
|
||||
// settings-scope compares the selected profile against the roster's default; 'work' IS the
|
||||
// default here so the settings surfaces render without the non-default warning.
|
||||
return {
|
||||
$activeGatewayProfile: atom<string>('work'),
|
||||
$profiles: atom([{ name: 'work', is_default: true }]),
|
||||
normalizeProfileKey: (name: string | null | undefined): string => (name ?? '').trim() || 'default'
|
||||
}
|
||||
})
|
||||
vi.mock('@/store/session', async (): Promise<object> => {
|
||||
const { atom } = await import('nanostores')
|
||||
|
||||
Reference in New Issue
Block a user