209 lines
7.0 KiB
TypeScript
209 lines
7.0 KiB
TypeScript
// src/services/aiAnalysisService.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as aiApiClient from './aiApiClient';
|
|
import { AiAnalysisService } from './aiAnalysisService';
|
|
import { createMockFlyerItem } from '../tests/utils/mockFactories';
|
|
|
|
// Mock the dependencies
|
|
vi.mock('./aiApiClient');
|
|
vi.mock('./logger.client', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('AiAnalysisService', () => {
|
|
let service: AiAnalysisService;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = new AiAnalysisService();
|
|
});
|
|
|
|
describe('getQuickInsights', () => {
|
|
it('should return insights string on success', async () => {
|
|
const mockResponse = 'Quick insights result';
|
|
vi.mocked(aiApiClient.getQuickInsights).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.getQuickInsights([]);
|
|
expect(result).toBe(mockResponse);
|
|
});
|
|
});
|
|
|
|
describe('getDeepDiveAnalysis', () => {
|
|
it('should return analysis string on success', async () => {
|
|
const mockResponse = 'Deep dive result';
|
|
vi.mocked(aiApiClient.getDeepDiveAnalysis).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.getDeepDiveAnalysis([]);
|
|
expect(result).toBe(mockResponse);
|
|
});
|
|
});
|
|
|
|
describe('searchWeb', () => {
|
|
it('should return a grounded response on success', async () => {
|
|
const mockResponse = {
|
|
text: 'Search results',
|
|
sources: [{ web: { uri: 'https://example.com', title: 'Example' } }],
|
|
};
|
|
vi.mocked(aiApiClient.searchWeb).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
|
|
|
expect(result.text).toBe('Search results');
|
|
expect(result.sources).toEqual([{ uri: 'https://example.com', title: 'Example' }]);
|
|
});
|
|
|
|
it('should handle responses with missing or empty sources array', async () => {
|
|
const mockResponse = { text: 'Search results', sources: null };
|
|
vi.mocked(aiApiClient.searchWeb).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
|
|
|
expect(result.text).toBe('Search results');
|
|
expect(result.sources).toEqual([]);
|
|
});
|
|
|
|
it('should handle source objects without a "web" property', async () => {
|
|
const mockResponse = {
|
|
text: 'Search results',
|
|
sources: [{ some_other_property: 'value' }],
|
|
};
|
|
vi.mocked(aiApiClient.searchWeb).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
|
|
|
expect(result.sources).toEqual([{ uri: '', title: 'Untitled' }]);
|
|
});
|
|
|
|
it('should re-throw an error if the API call fails', async () => {
|
|
const apiError = new Error('API is down');
|
|
vi.mocked(aiApiClient.searchWeb).mockRejectedValue(apiError);
|
|
|
|
await expect(service.searchWeb([createMockFlyerItem({ item: 'test' })])).rejects.toThrow(
|
|
apiError,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('planTripWithMaps', () => {
|
|
const mockGeolocation = {
|
|
getCurrentPosition: vi.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Mock the global navigator object
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
geolocation: mockGeolocation,
|
|
},
|
|
writable: true,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up the global mock
|
|
vi.stubGlobal('navigator', undefined);
|
|
});
|
|
|
|
it('should call the API with the fetched location on success', async () => {
|
|
const mockCoords = { latitude: 45, longitude: -75 };
|
|
mockGeolocation.getCurrentPosition.mockImplementationOnce((success) => {
|
|
success({ coords: mockCoords });
|
|
});
|
|
|
|
vi.mocked(aiApiClient.planTripWithMaps).mockResolvedValue({
|
|
json: () => Promise.resolve({ text: 'Trip planned!', sources: [] }),
|
|
} as Response);
|
|
|
|
const result = await service.planTripWithMaps([], undefined);
|
|
|
|
expect(mockGeolocation.getCurrentPosition).toHaveBeenCalledTimes(1);
|
|
expect(aiApiClient.planTripWithMaps).toHaveBeenCalledWith([], undefined, mockCoords);
|
|
expect(result.text).toBe('Trip planned!');
|
|
});
|
|
|
|
it('should reject if geolocation is not supported', async () => {
|
|
// Undefine geolocation for this test
|
|
Object.defineProperty(global, 'navigator', {
|
|
value: {
|
|
geolocation: undefined,
|
|
},
|
|
writable: true,
|
|
});
|
|
|
|
await expect(service.planTripWithMaps([], undefined)).rejects.toThrow(
|
|
'Geolocation is not supported by your browser.',
|
|
);
|
|
expect(aiApiClient.planTripWithMaps).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject if the user denies geolocation permission', async () => {
|
|
// Create a mock object that conforms to the GeolocationPositionError interface.
|
|
const permissionError = {
|
|
code: 1, // PERMISSION_DENIED
|
|
message: 'User denied Geolocation',
|
|
};
|
|
mockGeolocation.getCurrentPosition.mockImplementationOnce((_, error) => {
|
|
if (error) {
|
|
error(permissionError as GeolocationPositionError);
|
|
}
|
|
});
|
|
|
|
await expect(service.planTripWithMaps([], undefined)).rejects.toEqual(permissionError);
|
|
expect(aiApiClient.planTripWithMaps).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('compareWatchedItemPrices', () => {
|
|
it('should return grounded response on success', async () => {
|
|
const mockResponse = { text: 'Comparison', sources: [] };
|
|
vi.mocked(aiApiClient.compareWatchedItemPrices).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.compareWatchedItemPrices([]);
|
|
expect(result.text).toBe('Comparison');
|
|
});
|
|
|
|
it('should re-throw an error if the API call fails', async () => {
|
|
const apiError = new Error('API is down');
|
|
vi.mocked(aiApiClient.compareWatchedItemPrices).mockRejectedValue(apiError);
|
|
|
|
await expect(service.compareWatchedItemPrices([])).rejects.toThrow(apiError);
|
|
});
|
|
});
|
|
|
|
describe('generateImageFromText', () => {
|
|
it('should return image string on success', async () => {
|
|
const mockResponse = 'base64image';
|
|
vi.mocked(aiApiClient.generateImageFromText).mockResolvedValue({
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await service.generateImageFromText('prompt');
|
|
expect(result).toBe(mockResponse);
|
|
});
|
|
|
|
it('should re-throw an error if the API call fails', async () => {
|
|
const apiError = new Error('API is down');
|
|
vi.mocked(aiApiClient.generateImageFromText).mockRejectedValue(apiError);
|
|
|
|
await expect(service.generateImageFromText('a prompt')).rejects.toThrow(apiError);
|
|
});
|
|
});
|
|
});
|