diff --git a/web-app/src/hooks/__tests__/useModelSources.test.ts b/web-app/src/hooks/__tests__/useModelSources.test.ts
index d010f7f8a..41e5985a8 100644
--- a/web-app/src/hooks/__tests__/useModelSources.test.ts
+++ b/web-app/src/hooks/__tests__/useModelSources.test.ts
@@ -25,6 +25,11 @@ vi.mock('@/services/models', () => ({
fetchModelCatalog: vi.fn(),
}))
+// Mock the sanitizeModelId function
+vi.mock('@/lib/utils', () => ({
+ sanitizeModelId: vi.fn((id: string) => id),
+}))
+
describe('useModelSources', () => {
let mockFetchModelCatalog: any
@@ -56,15 +61,19 @@ describe('useModelSources', () => {
const mockSources: CatalogModel[] = [
{
model_name: 'model-1',
- provider: 'provider-1',
description: 'First model',
- version: '1.0.0',
+ developer: 'provider-1',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
},
{
model_name: 'model-2',
- provider: 'provider-2',
description: 'Second model',
- version: '2.0.0',
+ developer: 'provider-2',
+ downloads: 200,
+ num_quants: 1,
+ quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
},
]
@@ -101,18 +110,22 @@ describe('useModelSources', () => {
const existingSources: CatalogModel[] = [
{
model_name: 'existing-model',
- provider: 'existing-provider',
description: 'Existing model',
- version: '1.0.0',
+ developer: 'existing-provider',
+ downloads: 50,
+ num_quants: 1,
+ quants: [{ model_id: 'existing-model-q4', path: '/path/existing', file_size: '1GB' }],
},
]
const newSources: CatalogModel[] = [
{
model_name: 'new-model',
- provider: 'new-provider',
description: 'New model',
- version: '2.0.0',
+ developer: 'new-provider',
+ downloads: 150,
+ num_quants: 1,
+ quants: [{ model_id: 'new-model-q4', path: '/path/new', file_size: '2GB' }],
},
]
@@ -138,24 +151,30 @@ describe('useModelSources', () => {
const existingSources: CatalogModel[] = [
{
model_name: 'duplicate-model',
- provider: 'old-provider',
description: 'Old version',
- version: '1.0.0',
+ developer: 'old-provider',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'duplicate-model-q4', path: '/path/old', file_size: '1GB' }],
},
{
model_name: 'unique-model',
- provider: 'provider',
description: 'Unique model',
- version: '1.0.0',
+ developer: 'provider',
+ downloads: 75,
+ num_quants: 1,
+ quants: [{ model_id: 'unique-model-q4', path: '/path/unique', file_size: '1GB' }],
},
]
const newSources: CatalogModel[] = [
{
model_name: 'duplicate-model',
- provider: 'new-provider',
description: 'New version',
- version: '2.0.0',
+ developer: 'new-provider',
+ downloads: 200,
+ num_quants: 1,
+ quants: [{ model_id: 'duplicate-model-q4-new', path: '/path/new', file_size: '2GB' }],
},
]
@@ -207,9 +226,11 @@ describe('useModelSources', () => {
const mockSources: CatalogModel[] = [
{
model_name: 'model-1',
- provider: 'provider-1',
description: 'Model 1',
- version: '1.0.0',
+ developer: 'provider-1',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
},
]
@@ -238,9 +259,11 @@ describe('useModelSources', () => {
const mockSources: CatalogModel[] = [
{
model_name: 'shared-model',
- provider: 'shared-provider',
description: 'Shared model',
- version: '1.0.0',
+ developer: 'shared-provider',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'shared-model-q4', path: '/path/shared', file_size: '1GB' }],
},
]
@@ -288,18 +311,22 @@ describe('useModelSources', () => {
const sources1: CatalogModel[] = [
{
model_name: 'model-1',
- provider: 'provider-1',
description: 'First batch',
- version: '1.0.0',
+ developer: 'provider-1',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }],
},
]
const sources2: CatalogModel[] = [
{
model_name: 'model-2',
- provider: 'provider-2',
description: 'Second batch',
- version: '2.0.0',
+ developer: 'provider-2',
+ downloads: 200,
+ num_quants: 1,
+ quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }],
},
]
@@ -338,9 +365,11 @@ describe('useModelSources', () => {
const mockSources: CatalogModel[] = [
{
model_name: 'recovery-model',
- provider: 'recovery-provider',
description: 'Recovery model',
- version: '1.0.0',
+ developer: 'recovery-provider',
+ downloads: 100,
+ num_quants: 1,
+ quants: [{ model_id: 'recovery-model-q4', path: '/path/recovery', file_size: '1GB' }],
},
]
diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx
index b27fb2e79..a45a1779a 100644
--- a/web-app/src/routes/hub/index.tsx
+++ b/web-app/src/routes/hub/index.tsx
@@ -132,7 +132,9 @@ function Hub() {
// Apply search filter
if (debouncedSearchValue.length) {
const fuse = new Fuse(filtered, searchOptions)
- filtered = fuse.search(debouncedSearchValue).map((result) => result.item)
+ // Remove domain from search value (e.g., "huggingface.co/author/model" -> "author/model")
+ const cleanedSearchValue = debouncedSearchValue.replace(/^https?:\/\/[^/]+\//, '')
+ filtered = fuse.search(cleanedSearchValue).map((result) => result.item)
}
// Apply downloaded filter
if (showOnlyDownloaded) {
diff --git a/web-app/src/routes/settings/__tests__/general.test.tsx b/web-app/src/routes/settings/__tests__/general.test.tsx
index f5033b30b..96388b0fb 100644
--- a/web-app/src/routes/settings/__tests__/general.test.tsx
+++ b/web-app/src/routes/settings/__tests__/general.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
import { Route as GeneralRoute } from '../general'
// Mock all the dependencies
@@ -68,9 +68,12 @@ vi.mock('@/hooks/useGeneralSetting', () => ({
}),
}))
+// Create a controllable mock
+const mockCheckForUpdate = vi.fn()
+
vi.mock('@/hooks/useAppUpdater', () => ({
useAppUpdater: () => ({
- checkForUpdate: vi.fn(),
+ checkForUpdate: mockCheckForUpdate,
}),
}))
@@ -184,12 +187,17 @@ vi.mock('@tauri-apps/plugin-opener', () => ({
revealItemInDir: vi.fn(),
}))
-vi.mock('@tauri-apps/api/webviewWindow', () => ({
- WebviewWindow: vi.fn().mockImplementation((label: string, options: any) => ({
+vi.mock('@tauri-apps/api/webviewWindow', () => {
+ const MockWebviewWindow = vi.fn().mockImplementation((label: string, options: any) => ({
once: vi.fn(),
setFocus: vi.fn(),
- })),
-}))
+ }))
+ MockWebviewWindow.getByLabel = vi.fn().mockReturnValue(null)
+
+ return {
+ WebviewWindow: MockWebviewWindow,
+ }
+})
vi.mock('@tauri-apps/api/event', () => ({
emit: vi.fn(),
@@ -244,6 +252,7 @@ global.window = {
core: {
api: {
relaunch: vi.fn(),
+ getConnectedServers: vi.fn().mockResolvedValue([]),
},
},
}
@@ -258,20 +267,26 @@ Object.assign(navigator, {
describe('General Settings Route', () => {
beforeEach(() => {
vi.clearAllMocks()
+ // Reset the mock to return a promise that resolves immediately by default
+ mockCheckForUpdate.mockResolvedValue(null)
})
- it('should render the general settings page', () => {
+ it('should render the general settings page', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
expect(screen.getByTestId('header-page')).toBeInTheDocument()
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
expect(screen.getByText('common:settings')).toBeInTheDocument()
})
- it('should render app version', () => {
+ it('should render app version', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
expect(screen.getByText('v1.0.0')).toBeInTheDocument()
})
@@ -284,64 +299,82 @@ describe('General Settings Route', () => {
// expect(screen.getByTestId('language-switcher')).toBeInTheDocument()
// })
- it('should render switches for experimental features and spell check', () => {
+ it('should render switches for experimental features and spell check', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const switches = screen.getAllByTestId('switch')
expect(switches.length).toBeGreaterThanOrEqual(2)
})
- it('should render huggingface token input', () => {
+ it('should render huggingface token input', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const input = screen.getByTestId('input')
expect(input).toBeInTheDocument()
expect(input).toHaveValue('test-token')
})
- it('should handle spell check toggle', () => {
+ it('should handle spell check toggle', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const switches = screen.getAllByTestId('switch')
expect(switches.length).toBeGreaterThan(0)
// Test that switches are interactive
- fireEvent.click(switches[0])
+ await act(async () => {
+ fireEvent.click(switches[0])
+ })
expect(switches[0]).toBeInTheDocument()
})
- it('should handle experimental features toggle', () => {
+ it('should handle experimental features toggle', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const switches = screen.getAllByTestId('switch')
expect(switches.length).toBeGreaterThan(0)
// Test that switches are interactive
if (switches.length > 1) {
- fireEvent.click(switches[1])
+ await act(async () => {
+ fireEvent.click(switches[1])
+ })
expect(switches[1]).toBeInTheDocument()
}
})
- it('should handle huggingface token change', () => {
+ it('should handle huggingface token change', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const input = screen.getByTestId('input')
expect(input).toBeInTheDocument()
// Test that input is interactive
- fireEvent.change(input, { target: { value: 'new-token' } })
+ await act(async () => {
+ fireEvent.change(input, { target: { value: 'new-token' } })
+ })
expect(input).toBeInTheDocument()
})
it('should handle check for updates', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const buttons = screen.getAllByTestId('button')
const checkUpdateButton = buttons.find((button) =>
@@ -350,7 +383,9 @@ describe('General Settings Route', () => {
if (checkUpdateButton) {
expect(checkUpdateButton).toBeInTheDocument()
- fireEvent.click(checkUpdateButton)
+ await act(async () => {
+ fireEvent.click(checkUpdateButton)
+ })
// Test that button is interactive
expect(checkUpdateButton).toBeInTheDocument()
}
@@ -358,7 +393,9 @@ describe('General Settings Route', () => {
it('should handle data folder display', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
// Test that component renders without errors
expect(screen.getByTestId('header-page')).toBeInTheDocument()
@@ -367,25 +404,31 @@ describe('General Settings Route', () => {
it('should handle copy to clipboard', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
// Test that component renders without errors
expect(screen.getByTestId('header-page')).toBeInTheDocument()
expect(screen.getByTestId('settings-menu')).toBeInTheDocument()
})
- it('should handle factory reset dialog', () => {
+ it('should handle factory reset dialog', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
expect(screen.getByTestId('dialog')).toBeInTheDocument()
expect(screen.getByTestId('dialog-trigger')).toBeInTheDocument()
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
})
- it('should render external links', () => {
+ it('should render external links', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
// Check for external links
const links = screen.getAllByRole('link')
@@ -394,7 +437,9 @@ describe('General Settings Route', () => {
it('should handle logs window opening', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const buttons = screen.getAllByTestId('button')
const openLogsButton = buttons.find((button) =>
@@ -404,14 +449,18 @@ describe('General Settings Route', () => {
if (openLogsButton) {
expect(openLogsButton).toBeInTheDocument()
// Test that button is interactive
- fireEvent.click(openLogsButton)
+ await act(async () => {
+ fireEvent.click(openLogsButton)
+ })
expect(openLogsButton).toBeInTheDocument()
}
})
it('should handle reveal logs folder', async () => {
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const buttons = screen.getAllByTestId('button')
const revealLogsButton = buttons.find((button) =>
@@ -421,26 +470,39 @@ describe('General Settings Route', () => {
if (revealLogsButton) {
expect(revealLogsButton).toBeInTheDocument()
// Test that button is interactive
- fireEvent.click(revealLogsButton)
+ await act(async () => {
+ fireEvent.click(revealLogsButton)
+ })
expect(revealLogsButton).toBeInTheDocument()
}
})
- it('should show correct file explorer text for Windows', () => {
+ it('should show correct file explorer text for Windows', async () => {
global.IS_WINDOWS = true
global.IS_MACOS = false
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
expect(
screen.getByText('settings:general.showInFileExplorer')
).toBeInTheDocument()
})
- it('should disable check for updates button when checking', () => {
+ it('should disable check for updates button when checking', async () => {
+ // Create a promise that we can control
+ let resolveUpdate: (value: any) => void
+ const updatePromise = new Promise((resolve) => {
+ resolveUpdate = resolve
+ })
+ mockCheckForUpdate.mockReturnValue(updatePromise)
+
const Component = GeneralRoute.component as React.ComponentType
- render()
+ await act(async () => {
+ render()
+ })
const buttons = screen.getAllByTestId('button')
const checkUpdateButton = buttons.find((button) =>
@@ -448,8 +510,22 @@ describe('General Settings Route', () => {
)
if (checkUpdateButton) {
- fireEvent.click(checkUpdateButton)
+ // Click the button but don't await it yet
+ act(() => {
+ fireEvent.click(checkUpdateButton)
+ })
+
+ // Now the button should be disabled while checking
expect(checkUpdateButton).toBeDisabled()
+
+ // Resolve the promise to finish the update check
+ await act(async () => {
+ resolveUpdate!(null)
+ await updatePromise
+ })
+
+ // Button should be enabled again
+ expect(checkUpdateButton).not.toBeDisabled()
}
})
})