From 080b6c5a744c2ccc1219c860a65984f738a42e39 Mon Sep 17 00:00:00 2001 From: Louis Le Date: Mon, 16 Sep 2024 22:34:51 +0700 Subject: [PATCH 1/2] fix: #1142 - Toggle off experimental toggle does not turn off gated features --- .../Settings/Advanced/DataFolder/index.tsx | 1 + .../Settings/Advanced/FactoryReset/index.tsx | 6 +- web/screens/Settings/Advanced/index.test.tsx | 137 ++++++++++++++++++ web/screens/Settings/Advanced/index.tsx | 70 +++++++-- 4 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 web/screens/Settings/Advanced/index.test.tsx diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 3bb059a87c..985dc65c3d 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -100,6 +100,7 @@ const DataFolder = () => {
{ recommended only if the application is in a corrupted state.

- diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx new file mode 100644 index 0000000000..5d2adb382c --- /dev/null +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom' +import Advanced from '.'; + +class ResizeObserverMock { + observe() { } + unobserve() { } + disconnect() { } +} + +global.ResizeObserver = ResizeObserverMock +// @ts-ignore +global.window.core = { + api: { + getAppConfigurations: () => jest.fn(), + updateAppConfiguration: () => jest.fn(), + relaunch: () => jest.fn(), + } +} + +const setSettingsMock = jest.fn() +// Mock useSettings hook +jest.mock('@/hooks/useSettings', () => ({ + __esModule: true, + useSettings: () => ({ + readSettings: () => ({ + run_mode: 'gpu', + experimental: false, + proxy: false, + gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }], + gpus_in_use: ['0'], + quick_ask: false + }), + setSettings: setSettingsMock, + }), +})); + +import * as toast from '@/containers/Toast'; +jest.mock('@/containers/Toast'); + +jest.mock('@janhq/core', () => ({ + __esModule: true, + ...jest.requireActual('@janhq/core'), + fs: { + rm: jest.fn(), + } +})); + +// @ts-ignore +global.isMac = false; +// @ts-ignore +global.isWindows = true; + + +describe('Advanced', () => { + it('renders the component', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('Experimental Mode')).toBeInTheDocument(); + expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument(); + expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument(); + expect(screen.getByText('Jan Data Folder')).toBeInTheDocument(); + expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument(); + }) + }); + + it('updates Experimental enabled', async () => { + render(); + await waitFor(() => { + const experimentalToggle = screen.getByTestId(/experimental-switch/i); + fireEvent.click(experimentalToggle!); + expect(experimentalToggle).toBeChecked(); + }) + }); + + it('clears logs', async () => { + const jestMock = jest.fn(); + jest.spyOn(toast, 'toaster').mockImplementation(jestMock); + + render(); + + await waitFor(() => { + const clearLogsButton = screen.getByTestId(/clear-logs/i); + expect(clearLogsButton).toBeInTheDocument(); + fireEvent.click(clearLogsButton); + expect(jestMock).toHaveBeenCalled(); + }) + }); + + it('toggles proxy enabled', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument(); + const proxyToggle = screen.getByTestId(/proxy-switch/i); + fireEvent.click(proxyToggle); + expect(proxyToggle).toBeChecked(); + }) + }); + + it('updates proxy settings', async () => { + render(); + await waitFor(() => { + const proxyToggle = screen.getByTestId(/proxy-switch/i); + fireEvent.click(proxyToggle); + const proxyInput = screen.getByTestId(/proxy-input/i); + fireEvent.change(proxyInput, { target: { value: 'http://proxy.com' } }); + expect(proxyInput).toHaveValue('http://proxy.com'); + }) + }); + + it('toggles ignore SSL certificates', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument(); + const ignoreSslToggle = screen.getByTestId(/ignore-ssl-switch/i); + fireEvent.click(ignoreSslToggle); + expect(ignoreSslToggle).toBeChecked(); + }) + }); + + it('renders DataFolder component', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('Jan Data Folder')).toBeInTheDocument(); + expect(screen.getByTestId(/jan-data-folder-input/i)).toBeInTheDocument(); + }) + }); + + it('renders FactoryReset component', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument(); + expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument(); + }) + }); +}); \ No newline at end of file diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index f132f81e77..1384f5688e 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -43,19 +43,10 @@ type GPU = { name: string } -const test = [ - { - id: 'test a', - vram: 2, - name: 'nvidia A', - }, - { - id: 'test', - vram: 2, - name: 'nvidia B', - }, -] - +/** + * Advanced Settings Screen + * @returns + */ const Advanced = () => { const [experimentalEnabled, setExperimentalEnabled] = useAtom( experimentalFeatureEnabledAtom @@ -69,7 +60,7 @@ const Advanced = () => { const [partialProxy, setPartialProxy] = useState(proxy) const [gpuEnabled, setGpuEnabled] = useState(false) - const [gpuList, setGpuList] = useState(test) + const [gpuList, setGpuList] = useState([]) const [gpusInUse, setGpusInUse] = useState([]) const [dropdownOptions, setDropdownOptions] = useState( null @@ -87,6 +78,9 @@ const Advanced = () => { return y['name'] }) + /** + * Handle proxy change + */ const onProxyChange = useCallback( (event: ChangeEvent) => { const value = event.target.value || '' @@ -100,6 +94,12 @@ const Advanced = () => { [setPartialProxy, setProxy] ) + /** + * Update Quick Ask Enabled + * @param e + * @param relaunch + * @returns void + */ const updateQuickAskEnabled = async ( e: boolean, relaunch: boolean = true @@ -111,6 +111,12 @@ const Advanced = () => { if (relaunch) window.core?.api?.relaunch() } + /** + * Update Vulkan Enabled + * @param e + * @param relaunch + * @returns void + */ const updateVulkanEnabled = async (e: boolean, relaunch: boolean = true) => { toaster({ title: 'Reload', @@ -123,11 +129,19 @@ const Advanced = () => { if (relaunch) window.location.reload() } + /** + * Update Experimental Enabled + * @param e + * @returns + */ const updateExperimentalEnabled = async ( e: ChangeEvent ) => { setExperimentalEnabled(e.target.checked) - if (e) return + + // If it checked, we don't need to do anything else + // Otherwise have to reset other settings + if (e.target.checked) return // It affects other settings, so we need to reset them const isRelaunch = quickAskEnabled || vulkanEnabled @@ -136,6 +150,9 @@ const Advanced = () => { if (isRelaunch) window.core?.api?.relaunch() } + /** + * useEffect to set GPU enabled if possible + */ useEffect(() => { const setUseGpuIfPossible = async () => { const settings = await readSettings() @@ -149,6 +166,10 @@ const Advanced = () => { setUseGpuIfPossible() }, [readSettings, setGpuList, setGpuEnabled, setGpusInUse, setVulkanEnabled]) + /** + * Clear logs + * @returns + */ const clearLogs = async () => { try { await fs.rm(`file://logs`) @@ -163,6 +184,11 @@ const Advanced = () => { }) } + /** + * Handle GPU Change + * @param gpuId + * @returns + */ const handleGPUChange = (gpuId: string) => { let updatedGpusInUse = [...gpusInUse] if (updatedGpusInUse.includes(gpuId)) { @@ -188,6 +214,9 @@ const Advanced = () => { const gpuSelectionPlaceHolder = gpuList.length > 0 ? 'Select GPU' : "You don't have any compatible GPU" + /** + * Handle click outside + */ useClickOutside(() => setOpen(false), null, [dropdownOptions, toggle]) return ( @@ -204,6 +233,7 @@ const Advanced = () => {

@@ -401,11 +431,13 @@ const Advanced = () => {
setProxyEnabled(!proxyEnabled)} />
:@:'} value={partialProxy} onChange={onProxyChange} @@ -428,6 +460,7 @@ const Advanced = () => {

setIgnoreSSL(e.target.checked)} /> @@ -448,6 +481,7 @@ const Advanced = () => {

{ toaster({ @@ -471,7 +505,11 @@ const Advanced = () => { Clear all logs from Jan app.

- From 726fc53d4016ebdd8057ac2cc65f10682730280a Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 17 Sep 2024 11:59:13 +0700 Subject: [PATCH 2/2] test: add tests --- web/screens/Settings/Advanced/index.test.tsx | 147 +++++++++++-------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx index 5d2adb382c..10ea810b11 100644 --- a/web/screens/Settings/Advanced/index.test.tsx +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' -import Advanced from '.'; +import Advanced from '.' class ResizeObserverMock { - observe() { } - unobserve() { } - disconnect() { } + observe() {} + unobserve() {} + disconnect() {} } global.ResizeObserver = ResizeObserverMock @@ -16,10 +16,11 @@ global.window.core = { getAppConfigurations: () => jest.fn(), updateAppConfiguration: () => jest.fn(), relaunch: () => jest.fn(), - } + }, } const setSettingsMock = jest.fn() + // Mock useSettings hook jest.mock('@/hooks/useSettings', () => ({ __esModule: true, @@ -30,108 +31,124 @@ jest.mock('@/hooks/useSettings', () => ({ proxy: false, gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }], gpus_in_use: ['0'], - quick_ask: false + quick_ask: false, }), setSettings: setSettingsMock, }), -})); +})) + +import * as toast from '@/containers/Toast' -import * as toast from '@/containers/Toast'; -jest.mock('@/containers/Toast'); +jest.mock('@/containers/Toast') jest.mock('@janhq/core', () => ({ __esModule: true, ...jest.requireActual('@janhq/core'), fs: { rm: jest.fn(), - } -})); + }, +})) +// Simulate a full advanced settings screen // @ts-ignore -global.isMac = false; +global.isMac = false // @ts-ignore -global.isWindows = true; - +global.isWindows = true describe('Advanced', () => { it('renders the component', async () => { - render(); + render() await waitFor(() => { - expect(screen.getByText('Experimental Mode')).toBeInTheDocument(); - expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument(); - expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument(); - expect(screen.getByText('Jan Data Folder')).toBeInTheDocument(); - expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument(); + expect(screen.getByText('Experimental Mode')).toBeInTheDocument() + expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument() + expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument() + expect(screen.getByText('Jan Data Folder')).toBeInTheDocument() + expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument() }) - }); + }) it('updates Experimental enabled', async () => { - render(); + render() + let experimentalToggle await waitFor(() => { - const experimentalToggle = screen.getByTestId(/experimental-switch/i); - fireEvent.click(experimentalToggle!); - expect(experimentalToggle).toBeChecked(); + experimentalToggle = screen.getByTestId(/experimental-switch/i) + fireEvent.click(experimentalToggle!) }) - }); + expect(experimentalToggle).toBeChecked() + }) - it('clears logs', async () => { - const jestMock = jest.fn(); - jest.spyOn(toast, 'toaster').mockImplementation(jestMock); + it('updates Experimental disabled', async () => { + render() - render(); + let experimentalToggle + await waitFor(() => { + experimentalToggle = screen.getByTestId(/experimental-switch/i) + fireEvent.click(experimentalToggle!) + }) + expect(experimentalToggle).not.toBeChecked() + }) + + it('clears logs', async () => { + const jestMock = jest.fn() + jest.spyOn(toast, 'toaster').mockImplementation(jestMock) + render() + let clearLogsButton await waitFor(() => { - const clearLogsButton = screen.getByTestId(/clear-logs/i); - expect(clearLogsButton).toBeInTheDocument(); - fireEvent.click(clearLogsButton); - expect(jestMock).toHaveBeenCalled(); + clearLogsButton = screen.getByTestId(/clear-logs/i) + fireEvent.click(clearLogsButton) }) - }); + expect(clearLogsButton).toBeInTheDocument() + expect(jestMock).toHaveBeenCalled() + }) it('toggles proxy enabled', async () => { - render(); + render() + let proxyToggle await waitFor(() => { - expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument(); - const proxyToggle = screen.getByTestId(/proxy-switch/i); - fireEvent.click(proxyToggle); - expect(proxyToggle).toBeChecked(); + expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument() + proxyToggle = screen.getByTestId(/proxy-switch/i) + fireEvent.click(proxyToggle) }) - }); + expect(proxyToggle).toBeChecked() + }) it('updates proxy settings', async () => { - render(); + render() + let proxyInput await waitFor(() => { - const proxyToggle = screen.getByTestId(/proxy-switch/i); - fireEvent.click(proxyToggle); - const proxyInput = screen.getByTestId(/proxy-input/i); - fireEvent.change(proxyInput, { target: { value: 'http://proxy.com' } }); - expect(proxyInput).toHaveValue('http://proxy.com'); + const proxyToggle = screen.getByTestId(/proxy-switch/i) + fireEvent.click(proxyToggle) + proxyInput = screen.getByTestId(/proxy-input/i) + fireEvent.change(proxyInput, { target: { value: 'http://proxy.com' } }) }) - }); + expect(proxyInput).toHaveValue('http://proxy.com') + }) it('toggles ignore SSL certificates', async () => { - render(); + render() + let ignoreSslToggle await waitFor(() => { - expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument(); - const ignoreSslToggle = screen.getByTestId(/ignore-ssl-switch/i); - fireEvent.click(ignoreSslToggle); - expect(ignoreSslToggle).toBeChecked(); + expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument() + ignoreSslToggle = screen.getByTestId(/ignore-ssl-switch/i) + fireEvent.click(ignoreSslToggle) }) - }); + expect(ignoreSslToggle).toBeChecked() + }) it('renders DataFolder component', async () => { - render(); + render() await waitFor(() => { - expect(screen.getByText('Jan Data Folder')).toBeInTheDocument(); - expect(screen.getByTestId(/jan-data-folder-input/i)).toBeInTheDocument(); + expect(screen.getByText('Jan Data Folder')).toBeInTheDocument() + expect(screen.getByTestId(/jan-data-folder-input/i)).toBeInTheDocument() }) - }); + }) it('renders FactoryReset component', async () => { - render(); + render() await waitFor(() => { - expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument(); - expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument(); + expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument() + expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument() }) - }); -}); \ No newline at end of file + }) +})