LibreChat/client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx
Danny Avila 0ef369af9b
📦 chore: npm audit bump (#12074)
* chore: npm audit

- Bumped versions for several packages: `@hono/node-server` to 1.19.10, `@tootallnate/once` to 3.0.1, `hono` to 4.12.5, `serialize-javascript` to 7.0.4, and `svgo` to 2.8.2.
- Removed deprecated `@trysound/sax` package from package-lock.json.
- Updated integrity hashes and resolved URLs in package-lock.json to reflect the new versions.

* chore: update dependencies and package versions

- Bumped `jest-environment-jsdom` to version 30.2.0 in both package.json and client/package.json.
- Updated related Jest packages to version 30.2.0 in package-lock.json, ensuring compatibility with the latest features and fixes.
- Added `svgo` package with version 2.8.2 to package.json for improved SVG optimization.

* chore: add @happy-dom/jest-environment and update test files

- Added `@happy-dom/jest-environment` version 20.8.3 to `package.json` and `package-lock.json` for improved testing environment.
- Updated test files to utilize the new Jest environment, replacing mock implementations of `window.location` with `window.history.replaceState` for better clarity and maintainability.
- Refactored tests in `SourcesErrorBoundary`, `useFocusChatEffect`, `AuthContext`, and `StartupLayout` to enhance reliability and reduce complexity.
2026-03-04 20:25:12 -05:00

338 lines
10 KiB
TypeScript

const mockNavigate = jest.fn();
const mockTextAreaRef = { current: { focus: jest.fn() } };
let mockLog: jest.SpyInstance;
jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
useNavigate: jest.fn(),
}));
// Import the component under test and its dependencies
import { renderHook } from '@testing-library/react';
import { useLocation, useNavigate } from 'react-router-dom';
import useFocusChatEffect from '../useFocusChatEffect';
import { logger } from '~/utils';
describe('useFocusChatEffect', () => {
// Reset mocks before each test
beforeEach(() => {
mockLog = jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.clearAllMocks();
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
// Mock window.matchMedia
window.matchMedia = jest.fn().mockImplementation((query) => ({
matches: query === '(hover: hover)', // Desktop has hover capability
media: '',
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
// Set default location mock
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: true },
});
// Set default window.location
window.history.replaceState({}, '', '/c/new');
});
afterEach(() => {
window.history.replaceState({}, '', '/');
});
describe('Basic functionality', () => {
test('should focus textarea when location.state.focusChat is true', () => {
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith('/c/new', {
replace: true,
state: {},
});
expect(mockLog).toHaveBeenCalled();
});
test('should not focus textarea when location.state.focusChat is false', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: false },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
expect(mockNavigate).not.toHaveBeenCalled();
});
test('should not focus textarea when textAreaRef.current is null', () => {
const nullTextAreaRef = { current: null };
renderHook(() => useFocusChatEffect(nullTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
});
test('should not focus textarea on touchscreen devices', () => {
window.matchMedia = jest.fn().mockImplementation((query) => ({
matches: query === '(pointer: coarse)', // Touchscreen has coarse pointer
media: '',
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalled();
});
});
describe('URL parameter handling', () => {
// Helper function to run tests with different URL scenarios
const testUrlScenario = ({
windowLocationSearch,
reactRouterSearch,
expectedUrl,
testDescription,
}: {
windowLocationSearch: string;
reactRouterSearch: string;
expectedUrl: string;
testDescription: string;
}) => {
test(`${testDescription}`, () => {
window.history.replaceState({}, '', `/c/new${windowLocationSearch}`);
// Mock React Router's location
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: reactRouterSearch,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
expectedUrl,
expect.objectContaining({
replace: true,
state: {},
}),
);
});
};
test('should use window.location.search instead of location.search', () => {
window.history.replaceState({}, '', '/c/new?agent_id=test_agent_id');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4o-mini',
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
// Should use window.location.search, not location.search
'/c/new?agent_id=test_agent_id',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123',
reactRouterSearch: '?endpoint=openAI&model=gpt-4',
expectedUrl: '/c/new?agent_id=agent123',
testDescription: 'should prioritize window.location.search with agent_id parameter',
});
testUrlScenario({
windowLocationSearch: '',
reactRouterSearch: '?endpoint=openAI&model=gpt-4',
expectedUrl: '/c/new',
testDescription: 'should use empty path when window.location.search is empty',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123&prompt=test',
reactRouterSearch: '',
expectedUrl: '/c/new?agent_id=agent123&prompt=test',
testDescription: 'should use window.location.search when React Router search is empty',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123',
reactRouterSearch: '?agent_id=differentAgent',
expectedUrl: '/c/new?agent_id=agent123',
testDescription:
'should use window.location.search even when both have agent_id but with different values',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent/with%20spaces&prompt=test%20query',
reactRouterSearch: '?endpoint=openAI',
expectedUrl: '/c/new?agent_id=agent/with%20spaces&prompt=test%20query',
testDescription: 'should handle URL parameters with special characters correctly',
});
testUrlScenario({
windowLocationSearch:
'?agent_id=agent123&prompt=test&model=gpt-4&temperature=0.7&max_tokens=1000',
reactRouterSearch: '?endpoint=openAI',
expectedUrl:
'/c/new?agent_id=agent123&prompt=test&model=gpt-4&temperature=0.7&max_tokens=1000',
testDescription: 'should handle multiple URL parameters correctly',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123&broken=param=with=equals',
reactRouterSearch: '?endpoint=openAI',
expectedUrl: '/c/new?agent_id=agent123&broken=param=with=equals',
testDescription: 'should pass through malformed URL parameters unchanged',
});
test('should handle navigation immediately after URL parameter changes', () => {
window.history.replaceState({}, '', '/c/new?endpoint=openAI&model=gpt-4');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: { focusChat: true },
});
const { rerender } = renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new?endpoint=openAI&model=gpt-4',
expect.objectContaining({
replace: true,
state: {},
}),
);
jest.clearAllMocks();
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new_changed',
search: '?endpoint=openAI&model=gpt-4',
state: { focusChat: true },
});
rerender();
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new_changed?agent_id=agent123',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
test('should handle undefined or null search params gracefully', () => {
window.history.replaceState({}, '', '/c/new');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: undefined,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
jest.clearAllMocks();
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: null,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
test('should handle navigation when location.state is null', () => {
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: null,
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
});
test('should handle navigation when location.state.focusChat is undefined', () => {
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: { someOtherProp: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
});
test('should handle navigation when both search params are empty', () => {
window.history.replaceState({}, '', '/c/new');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
});
});