📦 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.
This commit is contained in:
Danny Avila 2026-03-04 20:25:12 -05:00 committed by GitHub
parent 956f8fb6f0
commit 0ef369af9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 566 additions and 1208 deletions

View file

@ -122,6 +122,7 @@
"@babel/preset-env": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.22.15",
"@happy-dom/jest-environment": "^20.8.3",
"@tanstack/react-query-devtools": "^4.29.0",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
@ -144,7 +145,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
"jest-file-loader": "^1.0.3",
"jest-junit": "^16.0.0",
"postcss": "^8.4.31",

View file

@ -1,3 +1,6 @@
/**
* @jest-environment @happy-dom/jest-environment
*/
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
@ -11,15 +14,6 @@ const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
return <div data-testid="normal-component">{'Normal component'}</div>;
};
// Mock window.location.reload
const mockReload = jest.fn();
Object.defineProperty(window, 'location', {
value: {
reload: mockReload,
},
writable: true,
});
describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -53,6 +47,8 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
});
it('should reload page when refresh button is clicked', () => {
const reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {});
render(
<SourcesErrorBoundary>
<ThrowError shouldThrow={true} />
@ -62,6 +58,6 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => {
const refreshButton = screen.getByRole('button', { name: 'Reload the page' });
fireEvent.click(refreshButton);
expect(mockReload).toHaveBeenCalled();
expect(reloadSpy).toHaveBeenCalled();
});
});

View file

@ -39,14 +39,12 @@ describe('useFocusChatEffect', () => {
state: { focusChat: true },
});
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '',
},
writable: true,
});
// Set default window.location
window.history.replaceState({}, '', '/c/new');
});
afterEach(() => {
window.history.replaceState({}, '', '/');
});
describe('Basic functionality', () => {
@ -115,14 +113,7 @@ describe('useFocusChatEffect', () => {
testDescription: string;
}) => {
test(`${testDescription}`, () => {
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: windowLocationSearch,
},
writable: true,
});
window.history.replaceState({}, '', `/c/new${windowLocationSearch}`);
// Mock React Router's location
(useLocation as jest.Mock).mockReturnValue({
@ -144,13 +135,7 @@ describe('useFocusChatEffect', () => {
};
test('should use window.location.search instead of location.search', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=test_agent_id',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new?agent_id=test_agent_id');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
@ -223,13 +208,7 @@ describe('useFocusChatEffect', () => {
});
test('should handle navigation immediately after URL parameter changes', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new?endpoint=openAI&model=gpt-4');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
@ -249,13 +228,7 @@ describe('useFocusChatEffect', () => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new_changed',
@ -275,13 +248,7 @@ describe('useFocusChatEffect', () => {
});
test('should handle undefined or null search params gracefully', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: undefined,
},
writable: true,
});
window.history.replaceState({}, '', '/c/new');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
@ -301,14 +268,6 @@ describe('useFocusChatEffect', () => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: null,
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: null,
@ -327,13 +286,7 @@ describe('useFocusChatEffect', () => {
});
test('should handle navigation when location.state is null', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
@ -348,13 +301,7 @@ describe('useFocusChatEffect', () => {
});
test('should handle navigation when location.state.focusChat is undefined', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new?agent_id=agent123');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
@ -369,13 +316,7 @@ describe('useFocusChatEffect', () => {
});
test('should handle navigation when both search params are empty', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '',
},
writable: true,
});
window.history.replaceState({}, '', '/c/new');
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',

View file

@ -1,3 +1,6 @@
/**
* @jest-environment @happy-dom/jest-environment
*/
import React from 'react';
import { render, act } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
@ -105,31 +108,17 @@ function renderProviderLive() {
}
describe('AuthContextProvider — login onError redirect handling', () => {
const originalLocation = window.location;
beforeEach(() => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: { ...originalLocation, pathname: '/login', search: '', hash: '' },
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/login');
});
afterEach(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/');
});
it('preserves a valid redirect_to param across login failure', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=%2Fc%2Fabc123', hash: '' },
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123');
renderProvider();
@ -143,11 +132,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
});
it('drops redirect_to when it contains an absolute URL (open-redirect prevention)', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=https%3A%2F%2Fevil.com', hash: '' },
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/login?redirect_to=https%3A%2F%2Fevil.com');
renderProvider();
@ -159,11 +144,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
});
it('drops redirect_to when it points to /login (recursive redirect prevention)', () => {
Object.defineProperty(window, 'location', {
value: { pathname: '/login', search: '?redirect_to=%2Flogin', hash: '' },
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/login?redirect_to=%2Flogin');
renderProvider();
@ -186,15 +167,7 @@ describe('AuthContextProvider — login onError redirect handling', () => {
it('preserves redirect_to with query params and hash', () => {
const target = '/c/abc123?model=gpt-4#section';
Object.defineProperty(window, 'location', {
value: {
pathname: '/login',
search: `?redirect_to=${encodeURIComponent(target)}`,
hash: '',
},
writable: true,
configurable: true,
});
window.history.replaceState({}, '', `/login?redirect_to=${encodeURIComponent(target)}`);
renderProvider();
@ -209,33 +182,20 @@ describe('AuthContextProvider — login onError redirect handling', () => {
});
describe('AuthContextProvider — logout onSuccess/onError handling', () => {
const originalLocation = window.location;
const mockSetTokenHeader = jest.requireMock('librechat-data-provider').setTokenHeader;
beforeEach(() => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
...originalLocation,
pathname: '/c/some-chat',
search: '',
hash: '',
replace: jest.fn(),
},
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/c/some-chat');
});
afterEach(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/');
});
it('calls window.location.replace and setTokenHeader(undefined) when redirect is present', () => {
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
renderProvider();
act(() => {
@ -245,23 +205,25 @@ describe('AuthContextProvider — logout onSuccess/onError handling', () => {
});
});
expect(window.location.replace).toHaveBeenCalledWith(
'https://idp.example.com/logout?id_token_hint=abc',
);
expect(replaceSpy).toHaveBeenCalledWith('https://idp.example.com/logout?id_token_hint=abc');
expect(mockSetTokenHeader).toHaveBeenCalledWith(undefined);
});
it('does not call window.location.replace when redirect is absent', async () => {
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
renderProvider();
act(() => {
mockCapturedLogoutOptions.onSuccess({ message: 'Logout successful' });
});
expect(window.location.replace).not.toHaveBeenCalled();
expect(replaceSpy).not.toHaveBeenCalled();
});
it('does not trigger silentRefresh after OIDC redirect', () => {
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
renderProviderLive();
mockRefreshMutate.mockClear();
@ -272,7 +234,7 @@ describe('AuthContextProvider — logout onSuccess/onError handling', () => {
});
});
expect(window.location.replace).toHaveBeenCalled();
expect(replaceSpy).toHaveBeenCalled();
expect(mockRefreshMutate).not.toHaveBeenCalled();
});
});
@ -285,6 +247,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
afterEach(() => {
sessionStorage.clear();
window.history.replaceState({}, '', '/');
});
it('navigates to stored sessionStorage redirect after successful token refresh', () => {
@ -315,10 +278,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
it('navigates to current URL when no stored redirect exists', () => {
jest.useFakeTimers();
Object.defineProperty(window, 'location', {
value: { ...window.location, pathname: '/c/new', search: '' },
writable: true,
});
window.history.replaceState({}, '', '/c/new');
renderProviderLive();
@ -367,10 +327,7 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
it('falls back to current URL for unsafe stored redirect', () => {
jest.useFakeTimers();
Object.defineProperty(window, 'location', {
value: { ...window.location, pathname: '/c/new', search: '' },
writable: true,
});
window.history.replaceState({}, '', '/c/new');
sessionStorage.setItem(SESSION_KEY, 'https://evil.com/steal');
renderProviderLive();
@ -396,33 +353,18 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
});
describe('AuthContextProvider — logout error handling', () => {
const originalLocation = window.location;
beforeEach(() => {
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
...originalLocation,
pathname: '/c/some-chat',
search: '',
hash: '',
replace: jest.fn(),
},
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/c/some-chat');
});
afterEach(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
configurable: true,
});
window.history.replaceState({}, '', '/');
});
it('clears auth state on logout error without external redirect', () => {
jest.useFakeTimers();
const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {});
const { getByTestId } = renderProvider();
act(() => {
@ -432,7 +374,7 @@ describe('AuthContextProvider — logout error handling', () => {
jest.advanceTimersByTime(100);
});
expect(window.location.replace).not.toHaveBeenCalled();
expect(replaceSpy).not.toHaveBeenCalled();
expect(getByTestId('consumer').getAttribute('data-authenticated')).toBe('false');
jest.useRealTimers();
});

View file

@ -58,22 +58,17 @@ const createTestRouter = (initialEntry: string, isAuthenticated: boolean) =>
);
describe('StartupLayout — redirect race condition', () => {
const originalLocation = window.location;
beforeEach(() => {
sessionStorage.clear();
});
afterEach(() => {
Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
window.history.replaceState({}, '', '/');
jest.restoreAllMocks();
});
it('navigates to /c/new when authenticated with no pending redirect', async () => {
Object.defineProperty(window, 'location', {
value: { ...originalLocation, search: '' },
writable: true,
});
window.history.replaceState({}, '', '/login');
const router = createTestRouter('/login', true);
render(<RouterProvider router={router} />);
@ -84,10 +79,7 @@ describe('StartupLayout — redirect race condition', () => {
});
it('does NOT navigate to /c/new when redirect_to URL param is present', async () => {
Object.defineProperty(window, 'location', {
value: { ...originalLocation, search: '?redirect_to=%2Fc%2Fabc123' },
writable: true,
});
window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123');
const router = createTestRouter('/login?redirect_to=%2Fc%2Fabc123', true);
render(<RouterProvider router={router} />);
@ -98,10 +90,7 @@ describe('StartupLayout — redirect race condition', () => {
});
it('does NOT navigate to /c/new when sessionStorage redirect is present', async () => {
Object.defineProperty(window, 'location', {
value: { ...originalLocation, search: '' },
writable: true,
});
window.history.replaceState({}, '', '/login');
sessionStorage.setItem(SESSION_KEY, '/c/abc123');
const router = createTestRouter('/login', true);
@ -113,10 +102,7 @@ describe('StartupLayout — redirect race condition', () => {
});
it('does NOT navigate when not authenticated', async () => {
Object.defineProperty(window, 'location', {
value: { ...originalLocation, search: '' },
writable: true,
});
window.history.replaceState({}, '', '/login');
const router = createTestRouter('/login', false);
render(<RouterProvider router={router} />);

1526
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -169,7 +169,11 @@
"eslint": {
"ajv": "6.14.0"
},
"underscore": "1.13.8"
"underscore": "1.13.8",
"hono": "^4.12.4",
"@hono/node-server": "^1.19.10",
"serialize-javascript": "^7.0.3",
"svgo": "^2.8.2"
},
"nodemonConfig": {
"ignore": [