diff --git a/core/src/browser/core.test.ts b/core/src/browser/core.test.ts index 117298eb6..1bb80d9eb 100644 --- a/core/src/browser/core.test.ts +++ b/core/src/browser/core.test.ts @@ -25,7 +25,7 @@ describe('test core apis', () => { }, } const result = await joinPath(paths) - expect(globalThis.core.api.joinPath).toHaveBeenCalledWith(paths) + expect(globalThis.core.api.joinPath).toHaveBeenCalledWith({ args: paths }) expect(result).toBe('/path/one/path/two') }) @@ -37,7 +37,7 @@ describe('test core apis', () => { }, } const result = await openFileExplorer(path) - expect(globalThis.core.api.openFileExplorer).toHaveBeenCalledWith(path) + expect(globalThis.core.api.openFileExplorer).toHaveBeenCalledWith({ path }) expect(result).toBe('opened') }) diff --git a/core/src/browser/extension.test.ts b/core/src/browser/extension.test.ts index 879258876..b2a1d1e73 100644 --- a/core/src/browser/extension.test.ts +++ b/core/src/browser/extension.test.ts @@ -1,7 +1,5 @@ import { BaseExtension } from './extension' import { SettingComponentProps } from '../types' -import { getJanDataFolderPath, joinPath } from './core' -import { fs } from './fs' jest.mock('./core') jest.mock('./fs') @@ -90,18 +88,32 @@ describe('BaseExtension', () => { { key: 'setting2', controllerProps: { value: 'value2' } } as any, ] - ;(getJanDataFolderPath as jest.Mock).mockResolvedValue('/data') - ;(joinPath as jest.Mock).mockResolvedValue('/data/settings/TestExtension') - ;(fs.existsSync as jest.Mock).mockResolvedValue(false) - ;(fs.mkdir as jest.Mock).mockResolvedValue(undefined) - ;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined) + const localStorageMock = (() => { + let store: Record = {} + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value + }, + removeItem: (key: string) => { + delete store[key] + }, + clear: () => { + store = {} + }, + } + })() + + Object.defineProperty(global, 'localStorage', { + value: localStorageMock, + }) + const mock = jest.spyOn(localStorage, 'setItem') await baseExtension.registerSettings(settings) - expect(fs.mkdir).toHaveBeenCalledWith('/data/settings/TestExtension') - expect(fs.writeFileSync).toHaveBeenCalledWith( - '/data/settings/TestExtension', - JSON.stringify(settings, null, 2) + expect(mock).toHaveBeenCalledWith( + 'TestExtension', + JSON.stringify(settings) ) }) @@ -125,17 +137,15 @@ describe('BaseExtension', () => { ] jest.spyOn(baseExtension, 'getSettings').mockResolvedValue(settings) - ;(getJanDataFolderPath as jest.Mock).mockResolvedValue('/data') - ;(joinPath as jest.Mock).mockResolvedValue('/data/settings/TestExtension/settings.json') - ;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined) + const mockSetItem = jest.spyOn(localStorage, 'setItem') await baseExtension.updateSettings([ { key: 'setting1', controllerProps: { value: 'newValue' } } as any, ]) - expect(fs.writeFileSync).toHaveBeenCalledWith( - '/data/settings/TestExtension/settings.json', - JSON.stringify([{ key: 'setting1', controllerProps: { value: 'newValue' } }], null, 2) + expect(mockSetItem).toHaveBeenCalledWith( + 'TestExtension', + JSON.stringify([{ key: 'setting1', controllerProps: { value: 'newValue' } }]) ) }) }) diff --git a/core/src/browser/fs.test.ts b/core/src/browser/fs.test.ts index 21da54874..04e6fbe1c 100644 --- a/core/src/browser/fs.test.ts +++ b/core/src/browser/fs.test.ts @@ -36,31 +36,31 @@ describe('fs module', () => { it('should call readFileSync with correct arguments', () => { const args = ['path/to/file'] fs.readFileSync(...args) - expect(globalThis.core.api.readFileSync).toHaveBeenCalledWith(...args) + expect(globalThis.core.api.readFileSync).toHaveBeenCalledWith({ args }) }) it('should call existsSync with correct arguments', () => { const args = ['path/to/file'] fs.existsSync(...args) - expect(globalThis.core.api.existsSync).toHaveBeenCalledWith(...args) + expect(globalThis.core.api.existsSync).toHaveBeenCalledWith({ args }) }) it('should call readdirSync with correct arguments', () => { const args = ['path/to/directory'] fs.readdirSync(...args) - expect(globalThis.core.api.readdirSync).toHaveBeenCalledWith(...args) + expect(globalThis.core.api.readdirSync).toHaveBeenCalledWith({ args }) }) it('should call mkdir with correct arguments', () => { const args = ['path/to/directory'] fs.mkdir(...args) - expect(globalThis.core.api.mkdir).toHaveBeenCalledWith(...args) + expect(globalThis.core.api.mkdir).toHaveBeenCalledWith({ args }) }) it('should call rm with correct arguments', () => { const args = ['path/to/directory'] fs.rm(...args) - expect(globalThis.core.api.rm).toHaveBeenCalledWith(...args, { recursive: true, force: true }) + expect(globalThis.core.api.rm).toHaveBeenCalledWith({ args }) }) it('should call unlinkSync with correct arguments', () => { diff --git a/package.json b/package.json index 4516e86ba..388dea432 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dev:electron": "yarn copy:assets && yarn workspace jan dev", "dev:web:standalone": "concurrently \"yarn workspace @janhq/web dev\" \"wait-on http://localhost:3000 && rsync -av --prune-empty-dirs --include '*/' --include 'dist/***' --include 'package.json' --include 'tsconfig.json' --exclude '*' ./extensions/ web/.next/static/extensions/\"", "dev:web": "yarn workspace @janhq/web dev", + "dev:web:tauri": "IS_TAURI=true yarn workspace @janhq/web dev", "dev:server": "yarn workspace @janhq/server dev", "dev": "concurrently -n \"NEXT,ELECTRON\" -c \"yellow,blue\" --kill-others \"yarn dev:web\" \"yarn dev:electron\"", "install:cortex:linux:darwin": "cd src-tauri/binaries && ./download.sh", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d829d41fb..69e17f3e8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "frontendDist": "../web/out", "devUrl": "http://localhost:3000", - "beforeDevCommand": "yarn dev:web", + "beforeDevCommand": "yarn dev:web:tauri", "beforeBuildCommand": "yarn build:web" }, "app": { diff --git a/web/containers/ModalTroubleShoot/AppLogs.test.tsx b/web/containers/ModalTroubleShoot/AppLogs.test.tsx index 9f59b5dfa..7b5c957ba 100644 --- a/web/containers/ModalTroubleShoot/AppLogs.test.tsx +++ b/web/containers/ModalTroubleShoot/AppLogs.test.tsx @@ -70,7 +70,7 @@ describe('AppLogs Component', () => { const openButton = screen.getByText('Open') userEvent.click(openButton) - expect(mockOnRevealInFinder).toHaveBeenCalledWith('Logs') + expect(mockOnRevealInFinder).toHaveBeenCalledWith('logs') }) }) diff --git a/web/hooks/useDeleteThread.test.ts b/web/hooks/useDeleteThread.test.ts index 8d616cb42..bf53589ea 100644 --- a/web/hooks/useDeleteThread.test.ts +++ b/web/hooks/useDeleteThread.test.ts @@ -9,8 +9,6 @@ import { extensionManager } from '@/extension/ExtensionManager' import { useCreateNewThread } from './useCreateNewThread' import { Thread } from '@janhq/core/dist/types/types' import { currentPromptAtom } from '@/containers/Providers/Jotai' -import { setActiveThreadIdAtom, deleteThreadStateAtom } from '@/helpers/atoms/Thread.atom' -import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' // Mock the necessary dependencies // Mock dependencies jest.mock('jotai', () => ({ @@ -44,6 +42,7 @@ describe('useDeleteThread', () => { extensionManager.get = jest.fn().mockReturnValue({ deleteThread: mockDeleteThread, + getThreadAssistant: jest.fn().mockResolvedValue({}), }) const { result } = renderHook(() => useDeleteThread()) diff --git a/web/hooks/useFactoryReset.test.ts b/web/hooks/useFactoryReset.test.ts index a5b5844bc..c66ecce20 100644 --- a/web/hooks/useFactoryReset.test.ts +++ b/web/hooks/useFactoryReset.test.ts @@ -20,9 +20,7 @@ jest.mock('@janhq/core', () => ({ EngineManager: { instance: jest.fn().mockReturnValue({ get: jest.fn(), - engines: { - values: jest.fn().mockReturnValue([]), - }, + engines: {}, }), }, })) @@ -52,7 +50,8 @@ describe('useFactoryReset', () => { data_folder: '/current/jan/data/folder', quick_ask: false, }) - jest.spyOn(global, 'setTimeout') + // @ts-ignore + jest.spyOn(global, 'setTimeout').mockImplementation((cb) => cb()) }) it('should reset all correctly', async () => { @@ -69,15 +68,10 @@ describe('useFactoryReset', () => { FactoryResetState.StoppingModel ) expect(mockStopModel).toHaveBeenCalled() - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 4000) expect(mockSetFactoryResetState).toHaveBeenCalledWith( FactoryResetState.DeletingData ) - expect(fs.rm).toHaveBeenCalledWith('/current/jan/data/folder') - expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({ - data_folder: '/default/jan/data/folder', - quick_ask: false, - }) + expect(fs.rm).toHaveBeenCalledWith({ args: ['/current/jan/data/folder'] }) expect(mockSetFactoryResetState).toHaveBeenCalledWith( FactoryResetState.ClearLocalStorage ) @@ -92,6 +86,4 @@ describe('useFactoryReset', () => { expect(mockUpdateAppConfiguration).not.toHaveBeenCalled() }) - - // Add more tests as needed for error cases, edge cases, etc. }) diff --git a/web/hooks/useLoadTheme.test.ts b/web/hooks/useLoadTheme.test.ts index 8d352a52c..e17a67ed8 100644 --- a/web/hooks/useLoadTheme.test.ts +++ b/web/hooks/useLoadTheme.test.ts @@ -40,6 +40,11 @@ describe('useLoadTheme', () => { } it('should load theme and set variables', async () => { + global.window.core = { + api: { + getThemes: () => ['joi-light', 'joi-dark'], + }, + } // Mock Jotai hooks ;(useAtomValue as jest.Mock).mockImplementation((atom) => { switch (atom) { @@ -88,13 +93,10 @@ describe('useLoadTheme', () => { }) // Assertions - expect(fs.readdirSync).toHaveBeenCalledWith(mockThemesPath) - expect(fs.readFileSync).toHaveBeenCalledWith( - `${mockThemesPath}/${mockSelectedThemeId}/theme.json`, + expect(fs.readFileSync).toHaveBeenLastCalledWith( + `file://themes/joi-light/theme.json`, 'utf-8' ) - expect(mockSetTheme).toHaveBeenCalledWith('light') - expect(window.electronAPI.setNativeThemeLight).toHaveBeenCalled() }) it('should set default theme if no selected theme', async () => { diff --git a/web/next.config.js b/web/next.config.js index c5582dddb..c36eae42a 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -42,7 +42,7 @@ const nextConfig = { isWindows: process.platform === 'win32', isLinux: process.platform === 'linux', PLATFORM: JSON.stringify(process.platform), - IS_TAURI: true, + IS_TAURI: process.env.IS_TAURI === 'true', }), ] return config diff --git a/web/screens/Thread/index.test.tsx b/web/screens/Thread/index.test.tsx index 5277e82b5..6b4ecdc93 100644 --- a/web/screens/Thread/index.test.tsx +++ b/web/screens/Thread/index.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render } from '@testing-library/react' +import { act, render, screen, waitFor } from '@testing-library/react' import ThreadScreen from './index' import { useStarterScreen } from '../../hooks/useStarterScreen' import '@testing-library/jest-dom' @@ -17,22 +17,25 @@ global.API_BASE_URL = 'http://localhost:3000' describe('ThreadScreen', () => { it('renders OnDeviceStarterScreen when isShowStarterScreen is true', () => { - ;(useStarterScreen as jest.Mock).mockReturnValue({ - isShowStarterScreen: true, - extensionHasSettings: false, + act(() => { + ;(useStarterScreen as jest.Mock).mockReturnValue({ + isShowStarterScreen: true, + extensionHasSettings: false, + }) }) - const { getByText } = render() expect(getByText('Select a model to start')).toBeInTheDocument() }) - it('renders Thread panels when isShowStarterScreen is false', () => { + it('renders Thread panels when isShowStarterScreen is false', async () => { ;(useStarterScreen as jest.Mock).mockReturnValue({ isShowStarterScreen: false, extensionHasSettings: false, }) + await waitFor(() => { + render() - const { getByText } = render() - expect(getByText('Welcome!')).toBeInTheDocument() + expect(screen.getByText('Welcome!')).toBeInTheDocument() + }) }) }) diff --git a/web/services/restService.ts b/web/services/restService.ts index a311245ad..f136a562e 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -42,6 +42,6 @@ export const restAPI = { }, {}), openExternalUrl, // Jan Server URL - baseApiUrl: undefined, //process.env.API_BASE_URL ?? API_BASE_URL, + baseApiUrl: process.env.API_BASE_URL ?? API_BASE_URL, pollingInterval: 5000, }