mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-19 01:15:04 -05:00
* Core infrastructure for LLM integration. * Add support for a number of different AI generated reports on security, performance, and schema design on servers, databases, and schemas, as appropriate. * Add a Natural Language AI assistant to the Query Tool. * Add an AI Insights panel to the EXPLAIN tool in the Query Tool, to analyse and report on issues in query plans.
298 lines
7.6 KiB
JavaScript
298 lines
7.6 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import '@testing-library/jest-dom';
|
|
import { withTheme } from '../fake_theme';
|
|
import AIReport from '../../../pgadmin/llm/static/js/AIReport.jsx';
|
|
|
|
describe('AIReport Component', () => {
|
|
let ThemedAIReport;
|
|
|
|
beforeAll(() => {
|
|
ThemedAIReport = withTheme(AIReport);
|
|
|
|
// Mock window.getComputedStyle for dark mode detection
|
|
window.getComputedStyle = jest.fn().mockReturnValue({
|
|
color: 'rgb(212, 212, 212)',
|
|
backgroundColor: 'rgb(30, 30, 30)'
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('should render without crashing', () => {
|
|
const { container } = render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
expect(container).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show regenerate and download buttons', () => {
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Regenerate')).toBeInTheDocument();
|
|
expect(screen.getByText('Download')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should disable download button when no report exists', () => {
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
const downloadButton = screen.getByText('Download').closest('button');
|
|
expect(downloadButton).toBeDisabled();
|
|
});
|
|
|
|
it('should detect dark mode from body styles', async () => {
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
// Wait for dark mode detection to run
|
|
await waitFor(() => {
|
|
// The component should apply light colors in dark mode
|
|
// This would be verified by checking computed styles
|
|
}, { timeout: 1500 });
|
|
});
|
|
|
|
it('should handle light mode correctly', async () => {
|
|
// Mock light mode
|
|
window.getComputedStyle = jest.fn().mockReturnValue({
|
|
color: 'rgb(0, 0, 0)',
|
|
backgroundColor: 'rgb(255, 255, 255)'
|
|
});
|
|
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
// Component should apply dark colors in light mode
|
|
}, { timeout: 1500 });
|
|
});
|
|
|
|
it('should handle report generation error gracefully', async () => {
|
|
// Mock fetch to return error
|
|
global.fetch = jest.fn().mockRejectedValue(new Error('API Error'));
|
|
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
const regenerateButton = screen.getByText('Regenerate');
|
|
fireEvent.click(regenerateButton);
|
|
|
|
await waitFor(() => {
|
|
// Should show error message
|
|
// expect(screen.getByText(/error/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display progress during report generation', async () => {
|
|
// Mock SSE EventSource
|
|
const mockEventSource = {
|
|
addEventListener: jest.fn(),
|
|
close: jest.fn(),
|
|
onerror: null
|
|
};
|
|
|
|
global.EventSource = jest.fn(() => mockEventSource);
|
|
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
const regenerateButton = screen.getByText('Regenerate');
|
|
fireEvent.click(regenerateButton);
|
|
|
|
// Simulate SSE progress event
|
|
const onMessage = mockEventSource.addEventListener.mock.calls.find(
|
|
call => call[0] === 'message'
|
|
)?.[1];
|
|
|
|
if (onMessage) {
|
|
onMessage({
|
|
data: JSON.stringify({
|
|
type: 'progress',
|
|
stage: 'analyzing',
|
|
message: 'Analyzing database structure...',
|
|
completed: 1,
|
|
total: 5
|
|
})
|
|
});
|
|
}
|
|
|
|
await waitFor(() => {
|
|
// Progress should be visible
|
|
// expect(screen.getByText(/analyzing/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should support all report categories', () => {
|
|
const categories = ['security', 'performance', 'design'];
|
|
|
|
categories.forEach(category => {
|
|
const { unmount } = render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory={category}
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Regenerate')).toBeInTheDocument();
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
it('should support all report types', () => {
|
|
const types = [
|
|
{ type: 'server', props: { sid: 1, serverName: 'Test' } },
|
|
{ type: 'database', props: { sid: 1, did: 5, serverName: 'Test', databaseName: 'TestDB' } },
|
|
{ type: 'schema', props: { sid: 1, did: 5, scid: 10, serverName: 'Test', databaseName: 'TestDB', schemaName: 'public' } }
|
|
];
|
|
|
|
types.forEach(({ type, props }) => {
|
|
const { unmount } = render(
|
|
<ThemedAIReport
|
|
reportCategory="security"
|
|
reportType={type}
|
|
{...props}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Regenerate')).toBeInTheDocument();
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
it('should render markdown content correctly', () => {
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
// Would need to simulate report completion and verify markdown rendering
|
|
});
|
|
|
|
it('should handle download functionality', () => {
|
|
// Mock URL.createObjectURL
|
|
global.URL.createObjectURL = jest.fn(() => 'blob:mock-url');
|
|
global.URL.revokeObjectURL = jest.fn();
|
|
|
|
// Mock document.createElement for download link
|
|
const mockLink = {
|
|
click: jest.fn(),
|
|
setAttribute: jest.fn()
|
|
};
|
|
const createElementSpy = jest.spyOn(document, 'createElement').mockReturnValue(mockLink);
|
|
const appendChildSpy = jest.spyOn(document.body, 'appendChild').mockImplementation(() => {});
|
|
const removeChildSpy = jest.spyOn(document.body, 'removeChild').mockImplementation(() => {});
|
|
|
|
// Test would simulate having a report and clicking download
|
|
|
|
// Restore document mocks
|
|
createElementSpy.mockRestore();
|
|
appendChildSpy.mockRestore();
|
|
removeChildSpy.mockRestore();
|
|
});
|
|
|
|
it('should close EventSource on component unmount', () => {
|
|
const mockEventSource = {
|
|
addEventListener: jest.fn(),
|
|
close: jest.fn(),
|
|
onerror: null
|
|
};
|
|
|
|
global.EventSource = jest.fn(() => mockEventSource);
|
|
|
|
const { unmount } = render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
unmount();
|
|
|
|
// EventSource should be closed on unmount
|
|
// Would verify mockEventSource.close was called
|
|
});
|
|
|
|
it('should update text colors when theme changes', async () => {
|
|
render(
|
|
<ThemedAIReport
|
|
sid={1}
|
|
reportCategory="security"
|
|
reportType="server"
|
|
serverName="TestServer"
|
|
/>
|
|
);
|
|
|
|
// Change theme
|
|
window.getComputedStyle = jest.fn().mockReturnValue({
|
|
color: 'rgb(255, 255, 255)',
|
|
backgroundColor: 'rgb(0, 0, 0)'
|
|
});
|
|
|
|
// Wait for theme detection interval
|
|
await waitFor(() => {
|
|
// Colors should update
|
|
}, { timeout: 1500 });
|
|
});
|
|
});
|