brand new unit tests finally
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 2m27s

This commit is contained in:
2025-11-28 00:40:29 -08:00
parent bb8358200e
commit 4e6b605c8a
5 changed files with 56 additions and 37 deletions

View File

@@ -9,16 +9,17 @@ import { notifyError } from '../services/notificationService';
// The aiApiClient and notificationService are mocked globally via src/tests/setup/unit-setup.ts.
const mockedAiApiClient = aiApiClient as Mocked<typeof aiApiClient>;
// Mock the global Audio constructor
const mockAudioPlay = vi.fn(() => Promise.resolve()); // play() returns a promise
const mockAudio = vi.fn(() => ({
play: mockAudioPlay,
}));
vi.stubGlobal('Audio', mockAudio);
describe('VoiceLabPage', () => {
const mockAudioPlay = vi.fn(() => Promise.resolve());
beforeEach(() => {
vi.clearAllMocks();
// Mock the global Audio constructor freshly for each test
const mockAudio = vi.fn(() => ({
play: mockAudioPlay,
}));
vi.stubGlobal('Audio', mockAudio);
});
it('should render the initial state correctly', () => {
@@ -42,11 +43,11 @@ describe('VoiceLabPage', () => {
// Check for loading state
expect(generateButton).toBeDisabled();
expect(generateButton.querySelector('svg.animate-spin')).toBeInTheDocument();
await waitFor(() => {
expect(mockedAiApiClient.generateSpeechFromText).toHaveBeenCalledWith('Hello! This is a test of the text-to-speech generation.');
expect(mockAudio).toHaveBeenCalledWith(`data:audio/mpeg;base64,${mockBase64Audio}`);
// Expect global.Audio to have been called (Audio is now a spy)
expect(global.Audio).toHaveBeenCalledWith(`data:audio/mpeg;base64,${mockBase64Audio}`);
expect(mockAudioPlay).toHaveBeenCalled();
});

View File

@@ -1,8 +1,16 @@
// src/services/aiService.server.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import * as fs from 'fs/promises';
import type { MasterGroceryItem } from '../types';
// Mock fs/promises
const mockReadFile = vi.fn();
vi.mock('fs/promises', () => ({
default: {
readFile: (...args: any[]) => mockReadFile(...args),
},
readFile: (...args: any[]) => mockReadFile(...args),
}));
// Mock the Google GenAI library
const mockGenerateContent = vi.fn();
vi.mock('@google/genai', () => {
@@ -13,9 +21,6 @@ vi.mock('@google/genai', () => {
return { GoogleGenAI: mockGoogleGenAI };
});
// Spy on fs.readFile instead of mocking the whole module
const readFileSpy = vi.spyOn(fs, 'readFile');
// Mock the logger
vi.mock('./logger.server', () => ({
logger: {
@@ -42,7 +47,7 @@ describe('AI Service (Server)', () => {
]`,
};
mockGenerateContent.mockResolvedValue(mockResponse);
readFileSpy.mockResolvedValue(Buffer.from('mock-image-data'));
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
const result = await extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg');
@@ -56,7 +61,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.' });
readFileSpy.mockResolvedValue(Buffer.from('mock-image-data'));
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
await expect(extractItemsFromReceiptImage('path/to/image.jpg', 'image/jpeg')).rejects.toThrow(
'AI response did not contain a valid JSON array.'
@@ -79,7 +84,7 @@ describe('AI Service (Server)', () => {
],
};
mockGenerateContent.mockResolvedValue({ text: JSON.stringify(mockAiResponse) });
readFileSpy.mockResolvedValue(Buffer.from('mock-image-data'));
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
const result = await extractCoreDataFromFlyerImage([{ path: 'path/to/image.jpg', mimetype: 'image/jpeg' }], mockMasterItems);
@@ -95,7 +100,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' });
readFileSpy.mockResolvedValue(Buffer.from('mock-image-data'));
mockReadFile.mockResolvedValue(Buffer.from('mock-image-data'));
await expect(extractCoreDataFromFlyerImage([], mockMasterItems)).rejects.toThrow(
'AI response did not contain a valid JSON object.'

View File

@@ -2,15 +2,17 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock the nodemailer library BEFORE importing the email service.
// This ensures that when emailService.server.ts is imported, it gets our mock version of nodemailer.
const mockSendMail = vi.fn();
vi.mock('nodemailer', () => ({
default: {
createTransport: vi.fn(() => ({
sendMail: mockSendMail,
})),
},
}));
// Mock both default and named exports to cover all import styles
vi.mock('nodemailer', () => {
const createTransport = vi.fn(() => ({
sendMail: mockSendMail,
}));
return {
default: { createTransport },
createTransport,
};
});
// Mock the logger to prevent console output during tests
vi.mock('./logger.server', () => ({
@@ -32,14 +34,14 @@ describe('Email Service (Server)', () => {
describe('sendPasswordResetEmail', () => {
it('should call sendMail with the correct recipient, subject, and link', async () => {
// Ensure the mock result is set before import/invocation
mockSendMail.mockResolvedValue({ messageId: 'test-id' });
// Dynamically import the service to use the fresh mock
const { sendPasswordResetEmail } = await import('./emailService.server');
const to = 'test@example.com';
const resetLink = 'http://localhost:3000/reset/mock-token-123';
// Mock a successful email send
mockSendMail.mockResolvedValue({ messageId: 'test-id' });
await sendPasswordResetEmail(to, resetLink);
expect(mockSendMail).toHaveBeenCalledTimes(1);

View File

@@ -7,15 +7,18 @@
type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
const log = <T extends unknown[]>(level: LogLevel, message: string, ...args: T) => {
const logMessage = `[${level}] ${message}`;
console.log(logMessage, ...args);
};
// Export the logger object for use throughout the client-side application.
export const logger = {
info: <T extends unknown[]>(message: string, ...args: T) => log('INFO', message, ...args),
warn: <T extends unknown[]>(message: string, ...args: T) => log('WARN', message, ...args),
error: <T extends unknown[]>(message: string, ...args: T) => log('ERROR', message, ...args),
debug: <T extends unknown[]>(message: string, ...args: T) => log('DEBUG', message, ...args),
info: <T extends unknown[]>(message: string, ...args: T) => {
console.log(`[INFO] ${message}`, ...args);
},
warn: <T extends unknown[]>(message: string, ...args: T) => {
console.warn(`[WARN] ${message}`, ...args);
},
error: <T extends unknown[]>(message: string, ...args: T) => {
console.error(`[ERROR] ${message}`, ...args);
},
debug: <T extends unknown[]>(message: string, ...args: T) => {
console.debug(`[DEBUG] ${message}`, ...args);
},
};

View File

@@ -3,6 +3,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import toast from 'react-hot-toast';
import { notifySuccess, notifyError } from './notificationService';
// Mock react-hot-toast
vi.mock('react-hot-toast', () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe('notificationService', () => {
beforeEach(() => {
// Clear mock history before each test to ensure isolation.