splitting unit + integration testing apart attempt #1
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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...');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user