From 19a60bc973c929a844090c224514cfde4f8fc2f3 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 16 Oct 2024 09:45:28 +0700 Subject: [PATCH 1/3] test: added more test inside containers component (#3765) * test: wip several component under containers * remoce checkbox test containers * test: added more test to containers component --- web/containers/AutoLink/index.test.tsx | 43 ++++++ web/containers/BlankState/index.test.tsx | 38 ++++++ web/containers/Brand/Logo/Mark.test.tsx | 37 +++++ .../CenterPanelContainer/index.test.tsx | 56 ++++++++ web/containers/CopyInstruction/index.test.tsx | 65 +++++++++ web/containers/EngineSetting/index.test.tsx | 115 ++++++++++++++++ web/containers/Loader/Bubble.tsx | 9 -- web/containers/Loader/ModelStart.test.tsx | 47 +++++++ web/containers/LoadingModal/index.test.tsx | 47 +++++++ .../MainViewContainer/index.test.tsx | 56 ++++++++ .../ModelConfigInput/index.test.tsx | 85 ++++++++++++ web/containers/Providers/Responsive.test.tsx | 110 +++++++++++++++ web/containers/Providers/Theme.test.tsx | 24 ++++ .../RightPanelContainer/index.test.tsx | 126 ++++++++++++++++++ web/jest.config.js | 4 +- 15 files changed, 851 insertions(+), 11 deletions(-) create mode 100644 web/containers/AutoLink/index.test.tsx create mode 100644 web/containers/BlankState/index.test.tsx create mode 100644 web/containers/Brand/Logo/Mark.test.tsx create mode 100644 web/containers/CenterPanelContainer/index.test.tsx create mode 100644 web/containers/CopyInstruction/index.test.tsx create mode 100644 web/containers/EngineSetting/index.test.tsx delete mode 100644 web/containers/Loader/Bubble.tsx create mode 100644 web/containers/Loader/ModelStart.test.tsx create mode 100644 web/containers/LoadingModal/index.test.tsx create mode 100644 web/containers/MainViewContainer/index.test.tsx create mode 100644 web/containers/ModelConfigInput/index.test.tsx create mode 100644 web/containers/Providers/Responsive.test.tsx create mode 100644 web/containers/Providers/Theme.test.tsx create mode 100644 web/containers/RightPanelContainer/index.test.tsx diff --git a/web/containers/AutoLink/index.test.tsx b/web/containers/AutoLink/index.test.tsx new file mode 100644 index 0000000000..9f4610a80c --- /dev/null +++ b/web/containers/AutoLink/index.test.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import AutoLink from './index' + +describe('AutoLink Component', () => { + it('renders text without links correctly', () => { + const text = 'This is a test without links.' + render() + expect(screen.getByText(text)).toBeInTheDocument() + }) + + it('renders text with a single link correctly', () => { + const text = 'Check this link: https://example.com' + render() + const link = screen.getByText('https://example.com') + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', 'https://example.com') + expect(link).toHaveAttribute('target', 'blank') + }) + + it('renders text with multiple links correctly', () => { + const text = 'Visit https://example.com and http://test.com' + render() + const link1 = screen.getByText('https://example.com') + const link2 = screen.getByText('http://test.com') + expect(link1).toBeInTheDocument() + expect(link1).toHaveAttribute('href', 'https://example.com') + expect(link1).toHaveAttribute('target', 'blank') + expect(link2).toBeInTheDocument() + expect(link2).toHaveAttribute('href', 'http://test.com') + expect(link2).toHaveAttribute('target', 'blank') + }) + + it('renders text with a link without protocol correctly', () => { + const text = 'Visit example.com for more info.' + render() + const link = screen.getByText('example.com') + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', 'http://example.com') + expect(link).toHaveAttribute('target', 'blank') + }) +}) diff --git a/web/containers/BlankState/index.test.tsx b/web/containers/BlankState/index.test.tsx new file mode 100644 index 0000000000..53cb2ece74 --- /dev/null +++ b/web/containers/BlankState/index.test.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import BlankState from './index' + +describe('BlankState Component', () => { + it('renders title correctly', () => { + const title = 'Test Title' + render() + expect(screen.getByText(title)).toBeInTheDocument() + }) + + it('renders description correctly when provided', () => { + const title = 'Test Title' + const description = 'Test Description' + render() + expect(screen.getByText(description)).toBeInTheDocument() + }) + + it('does not render description when not provided', () => { + const title = 'Test Title' + render() + expect(screen.queryByText('Test Description')).not.toBeInTheDocument() + }) + + it('renders action correctly when provided', () => { + const title = 'Test Title' + const action = + render() + expect(screen.getByText('Test Action')).toBeInTheDocument() + }) + + it('does not render action when not provided', () => { + const title = 'Test Title' + render() + expect(screen.queryByText('Test Action')).not.toBeInTheDocument() + }) +}) diff --git a/web/containers/Brand/Logo/Mark.test.tsx b/web/containers/Brand/Logo/Mark.test.tsx new file mode 100644 index 0000000000..68df134c9a --- /dev/null +++ b/web/containers/Brand/Logo/Mark.test.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import LogoMark from './Mark' + +describe('LogoMark Component', () => { + it('renders with default width and height', () => { + render() + const image = screen.getByAltText('Jan - Logo') + expect(image).toBeInTheDocument() + expect(image).toHaveAttribute('width', '24') + expect(image).toHaveAttribute('height', '24') + }) + + it('renders with provided width and height', () => { + render() + const image = screen.getByAltText('Jan - Logo') + expect(image).toBeInTheDocument() + expect(image).toHaveAttribute('width', '48') + expect(image).toHaveAttribute('height', '48') + }) + + it('applies provided className', () => { + render() + const image = screen.getByAltText('Jan - Logo') + expect(image).toBeInTheDocument() + expect(image).toHaveClass('custom-class') + }) + + it('renders with the correct src and alt attributes', () => { + render() + const image = screen.getByAltText('Jan - Logo') + expect(image).toBeInTheDocument() + expect(image).toHaveAttribute('src', 'icons/app_icon.svg') + expect(image).toHaveAttribute('alt', 'Jan - Logo') + }) +}) diff --git a/web/containers/CenterPanelContainer/index.test.tsx b/web/containers/CenterPanelContainer/index.test.tsx new file mode 100644 index 0000000000..9e6fda0073 --- /dev/null +++ b/web/containers/CenterPanelContainer/index.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react' +import { useAtomValue } from 'jotai' +import CenterPanelContainer from './index' +import '@testing-library/jest-dom' + +// Mock useAtomValue from jotai +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useAtomValue: jest.fn(), +})) + +describe('CenterPanelContainer', () => { + it('renders with reduceTransparent set to true', () => { + // Mock reduceTransparentAtom to be true + ;(useAtomValue as jest.Mock).mockReturnValue(true) + + render( + +
Test Child
+
+ ) + + // Check that the container renders with no border or rounded corners + const container = screen.getByText('Test Child').parentElement + expect(container).not.toHaveClass('rounded-lg border') + }) + + it('renders with reduceTransparent set to false', () => { + // Mock reduceTransparentAtom to be false + ;(useAtomValue as jest.Mock).mockReturnValue(false) + + render( + +
Test Child
+
+ ) + + // Check that the container renders with border and rounded corners + const container = screen.getByText('Test Child').parentElement + expect(container).toHaveClass('rounded-lg border') + }) + + it('renders children correctly', () => { + // Mock reduceTransparentAtom to be true for this test + ;(useAtomValue as jest.Mock).mockReturnValue(true) + + render( + +
Child Content
+
+ ) + + // Verify that the child content is rendered + expect(screen.getByText('Child Content')).toBeInTheDocument() + }) +}) diff --git a/web/containers/CopyInstruction/index.test.tsx b/web/containers/CopyInstruction/index.test.tsx new file mode 100644 index 0000000000..2f00e4e37c --- /dev/null +++ b/web/containers/CopyInstruction/index.test.tsx @@ -0,0 +1,65 @@ +import { render, screen, fireEvent } from '@testing-library/react' +import { useAtom } from 'jotai' +import '@testing-library/jest-dom' +import CopyOverInstruction from './index' + +// Mock the `useAtom` hook from jotai +jest.mock('jotai', () => ({ + useAtom: jest.fn(), +})) + +describe('CopyOverInstruction', () => { + const setCopyOverInstructionEnabled = jest.fn() + + beforeEach(() => { + ;(useAtom as jest.Mock).mockImplementation(() => [ + false, + setCopyOverInstructionEnabled, + ]) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should render the component with the switch in the correct state', () => { + render() + + // Assert the text is rendered + expect( + screen.getByText(/Save instructions for new threads/i) + ).toBeInTheDocument() + + // Assert the switch is rendered and in the unchecked state + const switchInput = screen.getByRole('checkbox') + expect(switchInput).toBeInTheDocument() + expect(switchInput).not.toBeChecked() + }) + + it('should call setCopyOverInstructionEnabled when the switch is toggled', () => { + render() + + const switchInput = screen.getByRole('checkbox') + + // Simulate toggling the switch + fireEvent.click(switchInput) + + // Assert that the atom setter is called with true when checked + expect(setCopyOverInstructionEnabled).toHaveBeenCalledWith(true) + }) + + it('should reflect the updated state when the atom value changes', () => { + // Mock the atom to return true (enabled state) + ;(useAtom as jest.Mock).mockImplementation(() => [ + true, + setCopyOverInstructionEnabled, + ]) + + render() + + const switchInput = screen.getByRole('checkbox') + + // The switch should now be checked + expect(switchInput).toBeChecked() + }) +}) diff --git a/web/containers/EngineSetting/index.test.tsx b/web/containers/EngineSetting/index.test.tsx new file mode 100644 index 0000000000..140a36395a --- /dev/null +++ b/web/containers/EngineSetting/index.test.tsx @@ -0,0 +1,115 @@ +import { render } from '@testing-library/react' +import '@testing-library/jest-dom' +import EngineSetting from './index' +import SettingComponentBuilder from '@/containers/ModelSetting/SettingComponent' +import { SettingComponentProps } from '@janhq/core' + +// Mock the SettingComponentBuilder component +jest.mock('@/containers/ModelSetting/SettingComponent', () => + jest.fn(() => null) +) + +describe('EngineSetting', () => { + const mockComponentData: SettingComponentProps[] = [ + { + key: 'setting1', + title: 'Setting 1', + description: 'This is the first setting.', + controllerType: 'input', + controllerProps: { + placeholder: 'Enter text', + value: 'default text', + type: 'text', + }, + }, + { + key: 'setting2', + title: 'Setting 2', + description: 'This is the second setting.', + controllerType: 'slider', + controllerProps: { + min: 0, + max: 100, + step: 1, + value: 50, + }, + }, + { + key: 'setting3', + title: 'Setting 3', + description: 'This is the third setting.', + controllerType: 'checkbox', + controllerProps: { + value: true, + }, + }, + ] + + const onValueChangedMock = jest.fn() + + afterEach(() => { + jest.clearAllMocks() // Clear mocks after each test + }) + + it('renders SettingComponentBuilder with the correct props', () => { + render( + + ) + + // Check that SettingComponentBuilder is called with the correct props + expect(SettingComponentBuilder).toHaveBeenCalledWith( + { + componentProps: mockComponentData, + disabled: false, + onValueUpdated: onValueChangedMock, + }, + {} + ) + }) + + it('renders SettingComponentBuilder with disabled prop', () => { + render( + + ) + + // Check that SettingComponentBuilder is called with disabled=true + expect(SettingComponentBuilder).toHaveBeenCalledWith( + { + componentProps: mockComponentData, + disabled: true, + onValueUpdated: onValueChangedMock, + }, + {} + ) + }) + + it('calls onValueChanged when the value is updated', () => { + // Simulating value update in SettingComponentBuilder + ;(SettingComponentBuilder as jest.Mock).mockImplementation( + ({ onValueUpdated }) => { + // Simulate calling the value update handler + onValueUpdated('setting1', 'new value') + return null + } + ) + + render( + + ) + + // Assert that onValueChanged is called with the correct parameters + expect(onValueChangedMock).toHaveBeenCalledWith('setting1', 'new value') + }) +}) diff --git a/web/containers/Loader/Bubble.tsx b/web/containers/Loader/Bubble.tsx deleted file mode 100644 index 0cd438ce0c..0000000000 --- a/web/containers/Loader/Bubble.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function BubbleLoader() { - return ( -
- - - -
- ) -} diff --git a/web/containers/Loader/ModelStart.test.tsx b/web/containers/Loader/ModelStart.test.tsx new file mode 100644 index 0000000000..62e333da05 --- /dev/null +++ b/web/containers/Loader/ModelStart.test.tsx @@ -0,0 +1,47 @@ +import '@testing-library/jest-dom' +import { render, screen, act } from '@testing-library/react' +import ModelStart from './ModelStart' // Adjust the path based on your file structure +import { useActiveModel } from '@/hooks/useActiveModel' + +// Mock the useActiveModel hook +jest.mock('@/hooks/useActiveModel', () => ({ + useActiveModel: jest.fn(), +})) + +describe('ModelStart', () => { + const mockSetStateModel = jest.fn() + const mockModel = { id: 'test-model' } + + beforeEach(() => { + // Reset the mock implementation before each test + jest.clearAllMocks() + }) + + it('renders correctly when loading is false', () => { + ;(useActiveModel as jest.Mock).mockReturnValue({ + stateModel: { + loading: false, + state: 'start', + model: mockModel, + }, + }) + + render() + // Ensure the component returns null when not loading + expect(screen.queryByText(/Starting model/i)).toBeNull() + }) + + it('renders loading state with model id', () => { + ;(useActiveModel as jest.Mock).mockReturnValue({ + stateModel: { + loading: true, + state: 'start', + model: mockModel, + }, + }) + + render() + // Ensure the loading text is rendered + expect(screen.getByText(/Starting model test-model/i)).toBeInTheDocument() + }) +}) diff --git a/web/containers/LoadingModal/index.test.tsx b/web/containers/LoadingModal/index.test.tsx new file mode 100644 index 0000000000..f5b17234d8 --- /dev/null +++ b/web/containers/LoadingModal/index.test.tsx @@ -0,0 +1,47 @@ +import '@testing-library/jest-dom' +import { render } from '@testing-library/react' +import { useAtomValue } from 'jotai' +import ResettingModal from './index' + +// Mocking the Jotai atom +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + + return { + ...originalModule, + useAtomValue: jest.fn(), + } +}) + +describe('ResettingModal', () => { + it('renders the modal with loading info when provided', () => { + const mockLoadingInfo = { + title: 'Loading...', + message: 'Please wait while we process your request.', + } + + // Mock the useAtomValue hook to return mock loading info + ;(useAtomValue as jest.Mock).mockReturnValue(mockLoadingInfo) + + const { getByText } = render() + + // Check if the modal title and message are displayed + expect(getByText('Loading...')).toBeInTheDocument() + expect( + getByText('Please wait while we process your request.') + ).toBeInTheDocument() + }) + + it('does not render the modal when loading info is undefined', () => { + // Mock the useAtomValue hook to return undefined + ;(useAtomValue as jest.Mock).mockReturnValue(undefined) + + const { queryByText } = render() + + // Check that the modal does not appear + expect(queryByText('Loading...')).not.toBeInTheDocument() + expect( + queryByText('Please wait while we process your request.') + ).not.toBeInTheDocument() + }) +}) diff --git a/web/containers/MainViewContainer/index.test.tsx b/web/containers/MainViewContainer/index.test.tsx new file mode 100644 index 0000000000..bcafa92afb --- /dev/null +++ b/web/containers/MainViewContainer/index.test.tsx @@ -0,0 +1,56 @@ +import '@testing-library/jest-dom' + +import { render } from '@testing-library/react' +import { useAtomValue } from 'jotai' +import MainViewContainer from './index' +import { MainViewState } from '@/constants/screens' + +// Mocking the Jotai atom +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + + return { + ...originalModule, + useAtomValue: jest.fn(), + } +}) + +// Mocking the screen components +jest.mock('@/screens/Hub', () => () =>
Hub Screen
) +jest.mock('@/screens/LocalServer', () => () =>
Local Server Screen
) +jest.mock('@/screens/Settings', () => () =>
Settings Screen
) +jest.mock('@/screens/Thread', () => () =>
Thread Screen
) + +describe('MainViewContainer', () => { + it('renders HubScreen when mainViewState is Hub', () => { + ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.Hub) + + const { getByText } = render() + + expect(getByText('Hub Screen')).toBeInTheDocument() + }) + + it('renders SettingsScreen when mainViewState is Settings', () => { + ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.Settings) + + const { getByText } = render() + + expect(getByText('Settings Screen')).toBeInTheDocument() + }) + + it('renders LocalServerScreen when mainViewState is LocalServer', () => { + ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.LocalServer) + + const { getByText } = render() + + expect(getByText('Local Server Screen')).toBeInTheDocument() + }) + + it('renders ThreadScreen when mainViewState is not defined', () => { + ;(useAtomValue as jest.Mock).mockReturnValue(undefined) + + const { getByText } = render() + + expect(getByText('Thread Screen')).toBeInTheDocument() + }) +}) diff --git a/web/containers/ModelConfigInput/index.test.tsx b/web/containers/ModelConfigInput/index.test.tsx new file mode 100644 index 0000000000..b92bdfcb23 --- /dev/null +++ b/web/containers/ModelConfigInput/index.test.tsx @@ -0,0 +1,85 @@ +import '@testing-library/jest-dom' +import React from 'react' +import { render, fireEvent } from '@testing-library/react' +import ModelConfigInput from './index' +import { Tooltip } from '@janhq/joi' + +// Mocking the Tooltip component to simplify testing +jest.mock('@janhq/joi', () => ({ + ...jest.requireActual('@janhq/joi'), + Tooltip: ({ + trigger, + content, + }: { + trigger: React.ReactNode + content: string + }) => ( +
+ {trigger} + {content} +
+ ), +})) + +describe('ModelConfigInput', () => { + it('renders correctly with given props', () => { + const { getByText, getByPlaceholderText } = render( + + ) + + // Check if title is rendered + expect(getByText('Test Title')).toBeInTheDocument() + + // Check if the description tooltip content is rendered + expect(getByText('This is a description.')).toBeInTheDocument() + + // Check if the placeholder is rendered + expect(getByPlaceholderText('Enter text here')).toBeInTheDocument() + }) + + it('calls onValueChanged when value changes', () => { + const onValueChangedMock = jest.fn() + const { getByPlaceholderText } = render( + + ) + + const textArea = getByPlaceholderText('Enter text here') + + // Simulate typing in the textarea + fireEvent.change(textArea, { target: { value: 'New Value' } }) + + // Check if onValueChanged was called with the new value + expect(onValueChangedMock).toHaveBeenCalledWith('New Value') + }) + + it('disables the textarea when disabled prop is true', () => { + const { getByPlaceholderText } = render( + + ) + + const textArea = getByPlaceholderText('Enter text here') + + // Check if the textarea is disabled + expect(textArea).toBeDisabled() + }) +}) diff --git a/web/containers/Providers/Responsive.test.tsx b/web/containers/Providers/Responsive.test.tsx new file mode 100644 index 0000000000..e72a5e7e62 --- /dev/null +++ b/web/containers/Providers/Responsive.test.tsx @@ -0,0 +1,110 @@ +import '@testing-library/jest-dom' +import React from 'react' +import { render } from '@testing-library/react' +import { useAtom } from 'jotai' +import Responsive from './Responsive' +import { showLeftPanelAtom, showRightPanelAtom } from '@/helpers/atoms/App.atom' + +// Mocking the required atoms +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + return { + ...originalModule, + useAtom: jest.fn(), + useAtomValue: jest.fn(), + } +}) + +const mockSetShowLeftPanel = jest.fn() +const mockSetShowRightPanel = jest.fn() +const mockShowLeftPanel = true +const mockShowRightPanel = true + +beforeEach(() => { + // Mocking the atom behavior + ;(useAtom as jest.Mock).mockImplementation((atom) => { + if (atom === showLeftPanelAtom) { + return [mockShowLeftPanel, mockSetShowLeftPanel] + } + if (atom === showRightPanelAtom) { + return [mockShowRightPanel, mockSetShowRightPanel] + } + return [null, jest.fn()] + }) +}) + +describe('Responsive', () => { + beforeAll(() => { + // Mocking the window.matchMedia function + window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, // Set this to true to simulate mobile view + addListener: jest.fn(), + removeListener: jest.fn(), + } + }) + }) + + it('renders children correctly', () => { + const { getByText } = render( + +
Child Content
+
+ ) + + // Check if the child content is rendered + expect(getByText('Child Content')).toBeInTheDocument() + }) + + it('hides left and right panels on small screens', () => { + // Simulate mobile view + window.matchMedia = jest.fn().mockImplementation((query) => ({ + matches: true, // Change to true to simulate mobile + addListener: jest.fn(), + removeListener: jest.fn(), + })) + + render( + +
Child Content
+
+ ) + + // Check that the left and right panel states were updated to false + expect(mockSetShowLeftPanel).toHaveBeenCalledWith(false) + expect(mockSetShowRightPanel).toHaveBeenCalledWith(false) + }) + + it('restores the last known panel states on larger screens', () => { + // Simulate mobile view first + window.matchMedia = jest.fn().mockImplementation((query) => ({ + matches: true, // Change to true to simulate mobile + addListener: jest.fn(), + removeListener: jest.fn(), + })) + + render( + +
Child Content
+
+ ) + + // Change back to desktop view + window.matchMedia = jest.fn().mockImplementation((query) => ({ + matches: false, // Change to false to simulate desktop + addListener: jest.fn(), + removeListener: jest.fn(), + })) + + // Call the effect manually to simulate the component re-rendering + const rerender = render( + +
Child Content
+
+ ) + + // Check that the last known states were restored (which were true initially) + expect(mockSetShowLeftPanel).toHaveBeenCalledWith(true) + expect(mockSetShowRightPanel).toHaveBeenCalledWith(true) + }) +}) diff --git a/web/containers/Providers/Theme.test.tsx b/web/containers/Providers/Theme.test.tsx new file mode 100644 index 0000000000..552bbecbe5 --- /dev/null +++ b/web/containers/Providers/Theme.test.tsx @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom' +import React from 'react' +import { render } from '@testing-library/react' +import ThemeWrapper from './Theme' + +// Mock the ThemeProvider from next-themes +jest.mock('next-themes', () => ({ + ThemeProvider: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})) + +describe('ThemeWrapper', () => { + it('renders children within ThemeProvider', () => { + const { getByText } = render( + +
Child Component
+
+ ) + + // Check if the child component is rendered + expect(getByText('Child Component')).toBeInTheDocument() + }) +}) diff --git a/web/containers/RightPanelContainer/index.test.tsx b/web/containers/RightPanelContainer/index.test.tsx new file mode 100644 index 0000000000..4bb08913f5 --- /dev/null +++ b/web/containers/RightPanelContainer/index.test.tsx @@ -0,0 +1,126 @@ +import '@testing-library/jest-dom' + +import React from 'react' +import { render, fireEvent } from '@testing-library/react' +import RightPanelContainer from './index' +import { useAtom } from 'jotai' + +// Mocking ResizeObserver +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +global.ResizeObserver = ResizeObserver + +// Mocking window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}) + +// Mocking the required atoms +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + return { + ...originalModule, + useAtom: jest.fn(), + useAtomValue: jest.fn(), + } +}) + +const mockSetShowRightPanel = jest.fn() +const mockShowRightPanel = true // Change this to test the panel visibility + +beforeEach(() => { + // Setting up the localStorage mock + localStorage.clear() + localStorage.setItem('rightPanelWidth', '280') // Setting a default width + + // Mocking the atom behavior + ;(useAtom as jest.Mock).mockImplementation(() => [ + mockShowRightPanel, + mockSetShowRightPanel, + ]) +}) + +describe('RightPanelContainer', () => { + it('renders correctly with children', () => { + const { getByText } = render( + +
Child Content
+
+ ) + + // Check if the child content is rendered + expect(getByText('Child Content')).toBeInTheDocument() + }) + + it('initializes width from localStorage', () => { + const { container } = render() + + // Check the width from localStorage is applied + const rightPanel = container.firstChild as HTMLDivElement + expect(rightPanel.style.width).toBe('280px') // Width from localStorage + }) + + it('changes width on resizing', () => { + const { container } = render() + + const rightPanel = container.firstChild as HTMLDivElement + + // Simulate mouse down on the resize handle + const resizeHandle = document.createElement('div') + resizeHandle.className = 'group/resize' + rightPanel.appendChild(resizeHandle) + + // Simulate mouse down to start resizing + fireEvent.mouseDown(resizeHandle) + + // Simulate mouse move event + fireEvent.mouseMove(window, { clientX: 100 }) + + // Simulate mouse up to stop resizing + fireEvent.mouseUp(window) + + // Verify that the right panel's width changes + // Since we can't get the actual width calculation in this test, + // you may want to check if the rightPanelWidth is updated in your implementation. + // Here, just check if the function is called: + expect(localStorage.getItem('rightPanelWidth')).toBeDefined() + }) + + it('hides panel when clicked outside on mobile', () => { + // Mock useMediaQuery to simulate mobile view + ;(window.matchMedia as jest.Mock).mockImplementation((query) => ({ + matches: true, // Always return true for mobile + addListener: jest.fn(), + removeListener: jest.fn(), + })) + + const { container } = render( + +
Child Content
+
+ ) + + const rightPanel = container.firstChild as HTMLDivElement + + // Simulate a click outside + fireEvent.mouseDown(document.body) + fireEvent.mouseUp(document.body) // Ensure the click event is completed + + // Verify that setShowRightPanel was called to hide the panel + expect(mockSetShowRightPanel).toHaveBeenCalledWith(false) + }) +}) diff --git a/web/jest.config.js b/web/jest.config.js index 7d2bee9ee5..12ed39b203 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -19,8 +19,8 @@ const config = { runner: './testRunner.js', collectCoverageFrom: ['./**/*.{ts,tsx}'], transform: { - "^.+\\.tsx?$": [ - "ts-jest", + '^.+\\.tsx?$': [ + 'ts-jest', { diagnostics: false, }, From 02190c5cbdb2734a8280ebdcdf91c0e5033432d9 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 16 Oct 2024 09:48:23 +0700 Subject: [PATCH 2/3] fix: reset window state after disconnected from multi monitor (#3797) --- electron/utils/setup.ts | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts index 437e21f977..39b8a41335 100644 --- a/electron/utils/setup.ts +++ b/electron/utils/setup.ts @@ -1,4 +1,4 @@ -import { app } from 'electron' +import { app, screen } from 'electron' import Store from 'electron-store' const DEFAULT_WIDTH = 1000 @@ -22,13 +22,42 @@ export const getBounds = async () => { height: DEFAULT_HEIGHT, } - const bounds = await storage.get('windowBounds') - if (bounds) { - return bounds as Electron.Rectangle - } else { + const bounds = (await storage.get('windowBounds')) as + | Electron.Rectangle + | undefined + + // If no bounds are saved, use the defaults + if (!bounds) { storage.set('windowBounds', defaultBounds) return defaultBounds } + + // Validate that the bounds are on a valid display + const displays = screen.getAllDisplays() + const isValid = displays.some((display) => { + const { x, y, width, height } = display.bounds + return ( + bounds.x >= x && + bounds.x < x + width && + bounds.y >= y && + bounds.y < y + height + ) + }) + + // If the position is valid, return the saved bounds, otherwise return default bounds + if (isValid) { + return bounds + } else { + const primaryDisplay = screen.getPrimaryDisplay() + const resetBounds = { + x: primaryDisplay.bounds.x, + y: primaryDisplay.bounds.y, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + } + storage.set('windowBounds', resetBounds) + return resetBounds + } } export const saveBounds = (bounds: Electron.Rectangle | undefined) => { From a2efa357fa310039f64ffd26485fa2ce2433eff1 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 16 Oct 2024 13:40:02 +0700 Subject: [PATCH 3/3] fix: enable scroll modal component (#3816) --- joi/src/core/Modal/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joi/src/core/Modal/styles.scss b/joi/src/core/Modal/styles.scss index fcbf071057..11af9418ae 100644 --- a/joi/src/core/Modal/styles.scss +++ b/joi/src/core/Modal/styles.scss @@ -13,7 +13,7 @@ fieldset, &__content { color: hsla(var(--modal-fg)); - overflow: hidden; + overflow: auto; background-color: hsla(var(--modal-bg)); border-radius: 8px; font-size: 14px;