splitting unit + integration testing apart attempt #1
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s

This commit is contained in:
2025-11-30 18:43:04 -08:00
parent ee74f396a8
commit e6c09e9c20
5 changed files with 67 additions and 50 deletions

View File

@@ -150,7 +150,7 @@ export const FlyerCorrectionTool: React.FC<FlyerCorrectionToolProps> = ({ isOpen
<div role="dialog" className="relative bg-gray-800 rounded-lg shadow-xl w-full max-w-6xl h-[90vh] flex flex-col" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center p-4 border-b border-gray-700">
<h2 className="text-lg font-semibold text-white flex items-center"><ScissorsIcon className="w-6 h-6 mr-2" /> Flyer Correction Tool</h2>
<button onClick={onClose} className="text-gray-400 hover:text-white"><XCircleIcon className="w-7 h-7" /></button>
<button onClick={onClose} className="text-gray-400 hover:text-white" aria-label="Close correction tool"><XCircleIcon className="w-7 h-7" /></button>
</div>
<div className="grow p-4 overflow-auto relative flex justify-center items-center">
<img ref={imageRef} src={imageUrl} alt="Flyer for correction" className="max-w-full max-h-full object-contain" onLoad={draw} />

View File

@@ -6,9 +6,18 @@ import type { ExecException, ChildProcess } from 'child_process';
import systemRouter from './system';
// Mock the child_process module. We explicitly define its exports.
// The mock factory MUST NOT reference variables from outside its scope due to hoisting.
// Instead, we import the mocked function after the mock is declared.
vi.mock('child_process', () => ({ exec: vi.fn() }));
// The mock needs to return a dummy ChildProcess object to match the real signature.
vi.mock('child_process', () => ({
exec: vi.fn((command: string, callback: (error: ExecException | null, stdout: string, stderr: string) => void) => {
// This default implementation can be overridden in tests.
// It simulates a successful execution with empty output.
if (callback) {
callback(null, '', '');
}
// Return a mock ChildProcess object.
return {} as ChildProcess;
}),
}));
// Now we can safely import the mocked function and control it.
import { exec } from 'child_process';

View File

@@ -14,17 +14,17 @@ vi.mock('fs/promises', () => ({
// Mock the Google GenAI library
const mockGenerateContent = vi.fn();
vi.mock('@google/generative-ai', () => ({
vi.mock('@google/generative-ai', () => {
// This mock structure correctly replicates the chained method calls:
// new GoogleGenerativeAI().getGenerativeModel().generateContent()
return {
GoogleGenerativeAI: vi.fn(() => ({
getGenerativeModel: vi.fn(() => ({
generateContent: mockGenerateContent.mockResolvedValue({
response: {
text: () => JSON.stringify([]), // Default successful response
},
}),
generateContent: mockGenerateContent,
})),
})),
}));
};
});
// Mock the sharp library
const mockToBuffer = vi.fn();
@@ -54,13 +54,12 @@ describe('AI Service (Server)', () => {
describe('extractItemsFromReceiptImage', () => {
it('should extract items from a valid AI response', async () => {
const { extractItemsFromReceiptImage } = await import('./aiService.server');
const mockResponse = {
text: `[
const mockAiResponseText = `[
{ "raw_item_description": "ORGANIC BANANAS", "price_paid_cents": 129 },
{ "raw_item_description": "AVOCADO", "price_paid_cents": 299 }
]`,
};
mockGenerateContent.mockResolvedValue(mockResponse);
]`;
// The AI SDK returns a response object with a `text()` method.
mockGenerateContent.mockResolvedValue({ response: { text: () => mockAiResponseText } });
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
const result = await extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg');
@@ -74,7 +73,7 @@ describe('AI Service (Server)', () => {
it('should throw an error if the AI response is not valid JSON', async () => {
const { extractItemsFromReceiptImage } = await import('./aiService.server');
mockGenerateContent.mockResolvedValue({ text: 'This is not JSON.' });
mockGenerateContent.mockResolvedValue({ response: { text: () => 'This is not JSON.' } });
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
await expect(extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg')).rejects.toThrow(
@@ -97,7 +96,7 @@ describe('AI Service (Server)', () => {
{ item: 'Oranges', price_display: null, price_in_cents: null, quantity: undefined, category_name: null, master_item_id: null },
],
};
mockGenerateContent.mockResolvedValue({ text: JSON.stringify(mockAiResponse) });
mockGenerateContent.mockResolvedValue({ response: { text: () => JSON.stringify(mockAiResponse) } });
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
const result = await extractCoreDataFromFlyerImage([{ path: 'path/to/image.jpg', mimetype: 'image/jpeg' }], mockMasterItems);
@@ -113,7 +112,7 @@ describe('AI Service (Server)', () => {
it('should throw an error if the AI response is not a valid JSON object', async () => {
const { extractCoreDataFromFlyerImage } = await import('./aiService.server');
mockGenerateContent.mockResolvedValue({ text: 'not a json object' });
mockGenerateContent.mockResolvedValue({ response: { text: () => 'not a json object' } });
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
await expect(extractCoreDataFromFlyerImage([], mockMasterItems)).rejects.toThrow(
@@ -125,18 +124,19 @@ describe('AI Service (Server)', () => {
describe('planTripWithMaps', () => {
it('should call generateContent and return the text and sources', async () => {
const { planTripWithMaps } = await import('./aiService.server');
const mockResponse = {
text: 'The nearest store is...',
candidates: [{
groundingMetadata: {
groundingChunks: [
{ web: { uri: 'http://maps.google.com/1', title: 'Map to Store A' } },
{ web: { uri: 'http://maps.google.com/2', title: 'Map to Store B' } },
],
},
}],
};
mockGenerateContent.mockResolvedValue(mockResponse);
mockGenerateContent.mockResolvedValue({
response: {
text: () => 'The nearest store is...',
candidates: [{
groundingMetadata: {
groundingChunks: [
{ web: { uri: 'http://maps.google.com/1', title: 'Map to Store A' } },
{ web: { uri: 'http://maps.google.com/2', title: 'Map to Store B' } },
],
},
}],
}
});
const mockLocation = { latitude: 48.4284, longitude: -123.3656 } as GeolocationCoordinates;
const result = await planTripWithMaps([], undefined, mockLocation);
@@ -144,7 +144,7 @@ describe('AI Service (Server)', () => {
expect(mockGenerateContent).toHaveBeenCalledTimes(1);
// Verify that the prompt includes the location data
const calledWith = mockGenerateContent.mock.calls[0][0];
expect(calledWith.contents).toContain('My current location is latitude 48.4284, longitude -123.3656');
expect(calledWith.contents[0].parts[0].text).toContain('My current location is latitude 48.4284, longitude -123.3656');
// Verify the returned structure
expect(result.text).toBe('The nearest store is...');

View File

@@ -1,7 +1,6 @@
// src/services/db/connection.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Pool } from 'pg';
import { mockPoolInstance, mockQuery } from '../../tests/setup/mock-db';
// Mock the logger
vi.mock('../logger', () => ({
@@ -11,6 +10,9 @@ vi.mock('../logger', () => ({
},
}));
// Since `pg` is mocked globally in `unit-setup.ts`, we can import it here
// to get access to the mocked constructor for our assertions.
describe('DB Connection Service', () => {
beforeEach(async () => {
vi.clearAllMocks();
@@ -24,8 +26,8 @@ describe('DB Connection Service', () => {
const { getPool } = await import('./connection');
const pool = getPool();
expect(Pool).toHaveBeenCalledTimes(1);
expect(pool).toBe(mockPoolInstance);
expect(vi.mocked(Pool)).toHaveBeenCalledTimes(1);
expect(pool).toBeDefined();
});
it('should return the same pool instance on subsequent calls', async () => {
@@ -34,18 +36,16 @@ describe('DB Connection Service', () => {
const pool2 = getPool();
// The Pool constructor should only be called once because of the singleton pattern.
expect(Pool).toHaveBeenCalledTimes(1);
expect(pool1).toBe(mockPoolInstance);
expect(pool2).toBe(mockPoolInstance);
expect(vi.mocked(Pool)).toHaveBeenCalledTimes(1);
expect(pool1).toBe(pool2);
});
});
describe('checkTablesExist', () => {
it('should return an empty array if all tables exist', async () => {
const { checkTablesExist } = await import('./connection');
const { getPool, checkTablesExist } = await import('./connection');
const tableNames = ['users', 'flyers'];
mockQuery.mockResolvedValue({ rows: [{ table_name: 'users' }, { table_name: 'flyers' }] });
const mockQuery = vi.mocked(getPool().query).mockResolvedValue({ rows: [{ table_name: 'users' }, { table_name: 'flyers' }] } as any);
const missingTables = await checkTablesExist(tableNames);
@@ -54,9 +54,9 @@ describe('DB Connection Service', () => {
});
it('should return an array of missing tables', async () => {
const { checkTablesExist } = await import('./connection');
const tableNames = ['users', 'flyers', 'products'];
mockQuery.mockResolvedValue({ rows: [{ table_name: 'users' }] });
const { getPool, checkTablesExist } = await import('./connection');
const mockQuery = vi.mocked(getPool().query).mockResolvedValue({ rows: [{ table_name: 'users' }] } as any);
const missingTables = await checkTablesExist(tableNames);

View File

@@ -56,17 +56,25 @@ afterEach(cleanup);
* This is the central point for mocking the database connection pool for unit tests.
*/
vi.mock('pg', () => {
const mockQuery = vi.fn();
const mockRelease = vi.fn();
const mockConnect = vi.fn().mockResolvedValue({ query: mockQuery, release: mockRelease });
const mockPool = vi.fn(() => ({ query: mockQuery, connect: mockConnect, totalCount: 10, idleCount: 5, waitingCount: 0, end: vi.fn() }));
// Define the mock pool instance that all tests will interact with.
const mPool = {
connect: vi.fn(),
query: vi.fn(),
end: vi.fn(),
on: vi.fn(),
totalCount: 10,
idleCount: 5,
waitingCount: 0,
};
return {
__esModule: true, // This is important for ES module interoperability.
Pool: mockPool,
// We are intentionally NOT exporting the mock functions. This was the source of the
// TypeScript errors. Test files will now mock the `getPool` function from the
// connection service directly.
// This correctly mocks the Pool so that `new Pool()` works in the application code.
// It returns our singleton mPool instance.
Pool: vi.fn(() => mPool),
types: {
setTypeParser: vi.fn(),
},
};
});