- {dimensions.width !== 'auto' && progress < 1 && (
-
- )}
+
diff --git a/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx
new file mode 100644
index 0000000000..3654d8e075
--- /dev/null
+++ b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx
@@ -0,0 +1,200 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import Image from '../Image';
+
+jest.mock('~/utils', () => ({
+ cn: (...classes: unknown[]) =>
+ classes
+ .flat(Infinity)
+ .filter((c) => typeof c === 'string' && c.length > 0)
+ .join(' '),
+}));
+
+jest.mock('librechat-data-provider', () => ({
+ apiBaseUrl: () => '',
+}));
+
+jest.mock('react-lazy-load-image-component', () => ({
+ LazyLoadImage: ({
+ alt,
+ src,
+ className,
+ onLoad,
+ placeholder,
+ visibleByDefault: _visibleByDefault,
+ ...rest
+ }: {
+ alt: string;
+ src: string;
+ className: string;
+ onLoad: () => void;
+ placeholder: React.ReactNode;
+ visibleByDefault?: boolean;
+ [key: string]: unknown;
+ }) => (
+
+

+
{placeholder}
+
+ ),
+}));
+
+jest.mock('@librechat/client', () => ({
+ Skeleton: ({ className, ...props }: React.HTMLAttributes
) => (
+
+ ),
+}));
+
+jest.mock('../DialogImage', () => ({
+ __esModule: true,
+ default: ({ isOpen, src }: { isOpen: boolean; src: string }) =>
+ isOpen ? : null,
+}));
+
+describe('Image', () => {
+ const defaultProps = {
+ imagePath: '/images/test.png',
+ altText: 'Test image',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('rendering', () => {
+ it('renders with max-h-[45vh] height constraint', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img.className).toContain('max-h-[45vh]');
+ });
+
+ it('renders with max-w-full to prevent landscape clipping', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img.className).toContain('max-w-full');
+ });
+
+ it('renders with w-auto and h-auto for natural aspect ratio', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img.className).toContain('w-auto');
+ expect(img.className).toContain('h-auto');
+ });
+
+ it('starts with opacity-0 before image loads', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img.className).toContain('opacity-0');
+ expect(img.className).not.toContain('opacity-100');
+ });
+
+ it('transitions to opacity-100 after image loads', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+
+ fireEvent.load(img);
+
+ expect(img.className).toContain('opacity-100');
+ });
+
+ it('applies custom className to the button wrapper', () => {
+ render();
+ const button = screen.getByRole('button');
+ expect(button.className).toContain('mb-4');
+ });
+
+ it('sets correct alt text', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img).toHaveAttribute('alt', 'Test image');
+ });
+ });
+
+ describe('skeleton placeholder', () => {
+ it('renders skeleton with non-zero dimensions', () => {
+ render();
+ const skeleton = screen.getByTestId('skeleton');
+ expect(skeleton.className).toContain('h-48');
+ expect(skeleton.className).toContain('w-full');
+ expect(skeleton.className).toContain('max-w-lg');
+ });
+
+ it('renders skeleton with max-h constraint', () => {
+ render();
+ const skeleton = screen.getByTestId('skeleton');
+ expect(skeleton.className).toContain('max-h-[45vh]');
+ });
+
+ it('has accessible loading attributes', () => {
+ render();
+ const skeleton = screen.getByTestId('skeleton');
+ expect(skeleton).toHaveAttribute('aria-label', 'Loading image');
+ expect(skeleton).toHaveAttribute('aria-busy', 'true');
+ });
+ });
+
+ describe('dialog interaction', () => {
+ it('opens dialog on button click after image loads', () => {
+ render();
+
+ const img = screen.getByTestId('lazy-image');
+ fireEvent.load(img);
+
+ expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument();
+
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+
+ expect(screen.getByTestId('dialog-image')).toBeInTheDocument();
+ });
+
+ it('does not render dialog before image loads', () => {
+ render();
+
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+
+ expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument();
+ });
+
+ it('has correct accessibility attributes on button', () => {
+ render();
+ const button = screen.getByRole('button');
+ expect(button).toHaveAttribute('aria-label', 'View Test image in dialog');
+ expect(button).toHaveAttribute('aria-haspopup', 'dialog');
+ });
+ });
+
+ describe('image URL resolution', () => {
+ it('passes /images/ paths through with base URL', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img).toHaveAttribute('src', '/images/test.png');
+ });
+
+ it('passes absolute http URLs through unchanged', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg');
+ });
+
+ it('passes data URIs through unchanged', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img).toHaveAttribute('src', 'data:image/png;base64,abc');
+ });
+
+ it('passes non-/images/ paths through unchanged', () => {
+ render();
+ const img = screen.getByTestId('lazy-image');
+ expect(img).toHaveAttribute('src', '/other/path.png');
+ });
+ });
+});
diff --git a/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx
new file mode 100644
index 0000000000..886b1b6294
--- /dev/null
+++ b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx
@@ -0,0 +1,182 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import OpenAIImageGen from '../Parts/OpenAIImageGen/OpenAIImageGen';
+
+jest.mock('~/utils', () => ({
+ cn: (...classes: unknown[]) =>
+ classes
+ .flat(Infinity)
+ .filter((c) => typeof c === 'string' && c.length > 0)
+ .join(' '),
+}));
+
+jest.mock('~/hooks', () => ({
+ useLocalize: () => (key: string) => key,
+}));
+
+jest.mock('~/components/Chat/Messages/Content/Image', () => ({
+ __esModule: true,
+ default: ({
+ altText,
+ imagePath,
+ className,
+ }: {
+ altText: string;
+ imagePath: string;
+ className?: string;
+ }) => (
+
+ ),
+}));
+
+jest.mock('@librechat/client', () => ({
+ PixelCard: ({ progress }: { progress: number }) => (
+
+ ),
+}));
+
+jest.mock('../Parts/OpenAIImageGen/ProgressText', () => ({
+ __esModule: true,
+ default: ({ progress, error }: { progress: number; error: boolean }) => (
+
+ ),
+}));
+
+describe('OpenAIImageGen', () => {
+ const defaultProps = {
+ initialProgress: 0.1,
+ isSubmitting: true,
+ toolName: 'image_gen_oai',
+ args: '{"prompt":"a cat","quality":"high","size":"1024x1024"}',
+ output: null as string | null,
+ attachments: undefined,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('image preloading', () => {
+ it('keeps Image mounted during generation (progress < 1)', () => {
+ render();
+ expect(screen.getByTestId('image-component')).toBeInTheDocument();
+ });
+
+ it('hides Image with invisible absolute while progress < 1', () => {
+ render();
+ const image = screen.getByTestId('image-component');
+ expect(image.className).toContain('invisible');
+ expect(image.className).toContain('absolute');
+ });
+
+ it('shows Image without hiding classes when progress >= 1', () => {
+ render(
+ ,
+ );
+ const image = screen.getByTestId('image-component');
+ expect(image.className).not.toContain('invisible');
+ expect(image.className).not.toContain('absolute');
+ });
+ });
+
+ describe('PixelCard visibility', () => {
+ it('shows PixelCard when progress < 1', () => {
+ render();
+ expect(screen.getByTestId('pixel-card')).toBeInTheDocument();
+ });
+
+ it('hides PixelCard when progress >= 1', () => {
+ render();
+ expect(screen.queryByTestId('pixel-card')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('layout classes', () => {
+ it('applies max-h-[45vh] to the outer container', () => {
+ const { container } = render();
+ const outerDiv = container.querySelector('[class*="max-h-"]');
+ expect(outerDiv?.className).toContain('max-h-[45vh]');
+ });
+
+ it('applies h-[45vh] w-full to inner container during loading', () => {
+ const { container } = render();
+ const innerDiv = container.querySelector('[class*="h-[45vh]"]');
+ expect(innerDiv).not.toBeNull();
+ expect(innerDiv?.className).toContain('w-full');
+ });
+
+ it('applies w-auto to inner container when complete', () => {
+ const { container } = render(
+ ,
+ );
+ const overflowDiv = container.querySelector('[class*="overflow-hidden"]');
+ expect(overflowDiv?.className).toContain('w-auto');
+ });
+ });
+
+ describe('args parsing', () => {
+ it('parses quality from args', () => {
+ render();
+ expect(screen.getByTestId('progress-text')).toBeInTheDocument();
+ });
+
+ it('handles invalid JSON args gracefully', () => {
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
+ render();
+ expect(screen.getByTestId('image-component')).toBeInTheDocument();
+ consoleSpy.mockRestore();
+ });
+
+ it('handles object args', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('image-component')).toBeInTheDocument();
+ });
+ });
+
+ describe('cancellation', () => {
+ it('shows error state when output contains error', () => {
+ render(
+ ,
+ );
+ const progressText = screen.getByTestId('progress-text');
+ expect(progressText).toHaveAttribute('data-error', 'true');
+ });
+
+ it('shows cancelled state when not submitting and incomplete', () => {
+ render();
+ const progressText = screen.getByTestId('progress-text');
+ expect(progressText).toHaveAttribute('data-error', 'true');
+ });
+ });
+});
diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts
index 8946951ed8..2a7dfc4a88 100644
--- a/client/src/utils/index.ts
+++ b/client/src/utils/index.ts
@@ -29,7 +29,6 @@ export * from './share';
export * from './timestamps';
export { default as cn } from './cn';
export { default as logger } from './logger';
-export { default as scaleImage } from './scaleImage';
export { default as getLoginError } from './getLoginError';
export { default as cleanupPreset } from './cleanupPreset';
export { default as buildDefaultConvo } from './buildDefaultConvo';
diff --git a/client/src/utils/scaleImage.ts b/client/src/utils/scaleImage.ts
deleted file mode 100644
index 11e051fbd9..0000000000
--- a/client/src/utils/scaleImage.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export default function scaleImage({
- originalWidth,
- originalHeight,
- containerRef,
-}: {
- originalWidth?: number;
- originalHeight?: number;
- containerRef: React.RefObject;
-}) {
- const containerWidth = containerRef.current?.offsetWidth ?? 0;
-
- if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
- return { width: 'auto', height: 'auto' };
- }
-
- const aspectRatio = originalWidth / originalHeight;
- const scaledWidth = Math.min(containerWidth, originalWidth);
- const scaledHeight = scaledWidth / aspectRatio;
-
- return { width: `${scaledWidth}px`, height: `${scaledHeight}px` };
-}