From 16bfd6eafbf3c38bb311fa06c32d0a7ca0140391 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 11:33:03 +0700 Subject: [PATCH] fix: full url search --- .../hooks/__tests__/useModelSources.test.ts | 77 ++++++--- web-app/src/routes/hub/index.tsx | 4 +- .../settings/__tests__/general.test.tsx | 156 +++++++++++++----- 3 files changed, 172 insertions(+), 65 deletions(-) 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() } }) })