Compare commits
2 Commits
dev
...
release/v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac3d6de26 | ||
|
|
25e3787677 |
@ -52,6 +52,16 @@ export default function LoadModelErrorDialog() {
|
||||
return copyText
|
||||
}
|
||||
|
||||
if (typeof error === 'object') {
|
||||
const errorObj = error as {
|
||||
code?: string
|
||||
message: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
return errorObj.message
|
||||
}
|
||||
|
||||
return JSON.stringify(error)
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,12 @@ Object.defineProperty(window, 'core', {
|
||||
writable: true,
|
||||
})
|
||||
|
||||
// Mock global AUTO_UPDATER_DISABLED
|
||||
Object.defineProperty(global, 'AUTO_UPDATER_DISABLED', {
|
||||
value: false,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
import { isDev } from '@/lib/utils'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import { events } from '@janhq/core'
|
||||
@ -251,11 +257,14 @@ describe('useAppUpdater', () => {
|
||||
downloadAndInstall: mockDownloadAndInstall,
|
||||
}
|
||||
|
||||
// Mock check to return the update
|
||||
mockCheck.mockResolvedValue(mockUpdate)
|
||||
|
||||
const { result } = renderHook(() => useAppUpdater())
|
||||
|
||||
// Set update info first
|
||||
act(() => {
|
||||
result.current.updateState.updateInfo = mockUpdate
|
||||
// Set update info first by calling checkForUpdate
|
||||
await act(async () => {
|
||||
await result.current.checkForUpdate()
|
||||
})
|
||||
|
||||
// Mock the download and install process
|
||||
@ -296,11 +305,14 @@ describe('useAppUpdater', () => {
|
||||
downloadAndInstall: mockDownloadAndInstall,
|
||||
}
|
||||
|
||||
// Mock check to return the update
|
||||
mockCheck.mockResolvedValue(mockUpdate)
|
||||
|
||||
const { result } = renderHook(() => useAppUpdater())
|
||||
|
||||
// Set update info first
|
||||
act(() => {
|
||||
result.current.updateState.updateInfo = mockUpdate
|
||||
// Set update info first by calling checkForUpdate
|
||||
await act(async () => {
|
||||
await result.current.checkForUpdate()
|
||||
})
|
||||
|
||||
mockDownloadAndInstall.mockRejectedValue(new Error('Download failed'))
|
||||
@ -338,11 +350,14 @@ describe('useAppUpdater', () => {
|
||||
downloadAndInstall: mockDownloadAndInstall,
|
||||
}
|
||||
|
||||
// Mock check to return the update
|
||||
mockCheck.mockResolvedValue(mockUpdate)
|
||||
|
||||
const { result } = renderHook(() => useAppUpdater())
|
||||
|
||||
// Set update info first
|
||||
act(() => {
|
||||
result.current.updateState.updateInfo = mockUpdate
|
||||
// Set update info first by calling checkForUpdate
|
||||
await act(async () => {
|
||||
await result.current.checkForUpdate()
|
||||
})
|
||||
|
||||
mockDownloadAndInstall.mockImplementation(async (progressCallback) => {
|
||||
|
||||
@ -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' }],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@ -53,6 +53,11 @@ export const useAppUpdater = () => {
|
||||
|
||||
const checkForUpdate = useCallback(
|
||||
async (resetRemindMeLater = false) => {
|
||||
if (AUTO_UPDATER_DISABLED) {
|
||||
console.log('Auto updater is disabled')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Reset remindMeLater if requested (e.g., when called from settings)
|
||||
if (resetRemindMeLater) {
|
||||
@ -148,6 +153,11 @@ export const useAppUpdater = () => {
|
||||
)
|
||||
|
||||
const downloadAndInstallUpdate = useCallback(async () => {
|
||||
if (AUTO_UPDATER_DISABLED) {
|
||||
console.log('Auto updater is disabled')
|
||||
return
|
||||
}
|
||||
|
||||
if (!updateState.updateInfo) return
|
||||
|
||||
try {
|
||||
|
||||
@ -2,6 +2,7 @@ import { create } from 'zustand'
|
||||
import { localStorageKey } from '@/constants/localStorage'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
import { fetchModelCatalog, CatalogModel } from '@/services/models'
|
||||
import { sanitizeModelId } from '@/lib/utils'
|
||||
|
||||
// Zustand store for model sources
|
||||
type ModelSourcesState = {
|
||||
@ -20,7 +21,15 @@ export const useModelSources = create<ModelSourcesState>()(
|
||||
fetchSources: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const newSources = await fetchModelCatalog()
|
||||
const newSources = await fetchModelCatalog().then((catalogs) =>
|
||||
catalogs.map((catalog) => ({
|
||||
...catalog,
|
||||
quants: catalog.quants.map((quant) => ({
|
||||
...quant,
|
||||
model_id: sanitizeModelId(quant.model_id),
|
||||
})),
|
||||
}))
|
||||
)
|
||||
|
||||
set({
|
||||
sources: newSources.length ? newSources : get().sources,
|
||||
|
||||
@ -155,3 +155,7 @@ export function formatDuration(startTime: number, endTime?: number): string {
|
||||
return `${durationMs}ms`
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeModelId(modelId: string): string {
|
||||
return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(/\./g, "_")
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
@ -194,7 +196,11 @@ function Hub() {
|
||||
if (repoInfo) {
|
||||
const catalogModel = convertHfRepoToCatalogModel(repoInfo)
|
||||
if (
|
||||
!sources.some((s) => s.model_name === catalogModel.model_name)
|
||||
!sources.some(
|
||||
(s) =>
|
||||
catalogModel.model_name.trim().split('/').pop() ===
|
||||
s.model_name.trim()
|
||||
)
|
||||
) {
|
||||
setHuggingFaceRepo(catalogModel)
|
||||
}
|
||||
|
||||
@ -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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
// 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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
// 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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
// 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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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(<Component />)
|
||||
await act(async () => {
|
||||
render(<Component />)
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -260,26 +260,28 @@ function General() {
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<CardItem
|
||||
title={t('settings:general.checkForUpdates')}
|
||||
description={t('settings:general.checkForUpdatesDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
onClick={handleCheckForUpdate}
|
||||
disabled={isCheckingUpdate}
|
||||
>
|
||||
<div className="cursor-pointer rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
|
||||
{isCheckingUpdate
|
||||
? t('settings:general.checkingForUpdates')
|
||||
: t('settings:general.checkForUpdates')}
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{!AUTO_UPDATER_DISABLED && (
|
||||
<CardItem
|
||||
title={t('settings:general.checkForUpdates')}
|
||||
description={t('settings:general.checkForUpdatesDesc')}
|
||||
className="flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-y-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
onClick={handleCheckForUpdate}
|
||||
disabled={isCheckingUpdate}
|
||||
>
|
||||
<div className="cursor-pointer rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
|
||||
{isCheckingUpdate
|
||||
? t('settings:general.checkingForUpdates')
|
||||
: t('settings:general.checkForUpdates')}
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* <CardItem
|
||||
title={t('common:language')}
|
||||
actions={<LanguageSwitcher />}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { sanitizeModelId } from '@/lib/utils'
|
||||
import {
|
||||
AIEngine,
|
||||
EngineManager,
|
||||
@ -165,7 +166,7 @@ export const convertHfRepoToCatalogModel = (
|
||||
const modelId = file.rfilename.replace(/\.gguf$/i, '')
|
||||
|
||||
return {
|
||||
model_id: modelId,
|
||||
model_id: sanitizeModelId(modelId),
|
||||
path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`,
|
||||
file_size: formatFileSize(file.size),
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user