diff --git a/src/App.test.tsx b/src/App.test.tsx
index 706c84c1..c62a9f81 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -25,6 +25,93 @@ import { useAppInitialization } from './hooks/useAppInitialization';
// Mock top-level components rendered by App's routes
+vi.mock('./components/Header', () => ({
+ Header: ({ onOpenProfile, onOpenVoiceAssistant }: any) => (
+
+
+
+
+ ),
+}));
+
+vi.mock('./components/Footer', () => ({
+ Footer: () => Mock Footer
,
+}));
+
+vi.mock('./layouts/MainLayout', async () => {
+ const { Outlet } = await vi.importActual('react-router-dom');
+ return {
+ MainLayout: () => (
+
+
+
+ ),
+ };
+});
+
+vi.mock('./pages/HomePage', () => ({
+ HomePage: ({ selectedFlyer, onOpenCorrectionTool }: any) => (
+
+
+
+ ),
+}));
+
+vi.mock('./pages/admin/AdminPage', () => ({
+ AdminPage: () => AdminPage
,
+}));
+
+vi.mock('./pages/admin/CorrectionsPage', () => ({
+ CorrectionsPage: () => CorrectionsPage
,
+}));
+
+vi.mock('./pages/admin/AdminStatsPage', () => ({
+ AdminStatsPage: () => AdminStatsPage
,
+}));
+
+vi.mock('./pages/admin/FlyerReviewPage', () => ({
+ FlyerReviewPage: () => FlyerReviewPage
,
+}));
+
+vi.mock('./pages/VoiceLabPage', () => ({
+ VoiceLabPage: () => VoiceLabPage
,
+}));
+
+vi.mock('./pages/ResetPasswordPage', () => ({
+ ResetPasswordPage: () => ResetPasswordPage
,
+}));
+
+vi.mock('./pages/admin/components/ProfileManager', () => ({
+ ProfileManager: ({ isOpen, onClose, onProfileUpdate, onLoginSuccess }: any) =>
+ isOpen ? (
+
+
+
+
+
+ ) : null,
+}));
+
+vi.mock('./features/voice-assistant/VoiceAssistant', () => ({
+ VoiceAssistant: ({ isOpen, onClose }: any) =>
+ isOpen ? (
+
+
+
+ ) : null,
+}));
+
+vi.mock('./components/FlyerCorrectionTool', () => ({
+ FlyerCorrectionTool: ({ isOpen, onClose, onDataExtracted }: any) =>
+ isOpen ? (
+
+
+
+
+
+ ) : null,
+}));
+
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
// This must be done in any test file that imports App.tsx.
vi.mock('pdfjs-dist', () => ({
@@ -128,6 +215,7 @@ describe('App Component', () => {
mockUseUserData.mockReturnValue({
watchedItems: [],
shoppingLists: [],
+ isLoadingShoppingLists: false,
setWatchedItems: vi.fn(),
setShoppingLists: vi.fn(),
});
diff --git a/src/services/authService.test.ts b/src/services/authService.test.ts
index b9a03de9..b54e69d9 100644
--- a/src/services/authService.test.ts
+++ b/src/services/authService.test.ts
@@ -2,7 +2,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { UserProfile } from '../types';
import type * as jsonwebtoken from 'jsonwebtoken';
-import { DatabaseError } from './processingErrors';
const { transactionalUserRepoMocks, transactionalAdminRepoMocks } = vi.hoisted(() => {
return {
@@ -33,6 +32,7 @@ describe('AuthService', () => {
let adminRepo: typeof import('./db/index.db').adminRepo;
let logger: typeof import('./logger.server').logger;
let sendPasswordResetEmail: typeof import('./emailService.server').sendPasswordResetEmail;
+ let DatabaseError: typeof import('./processingErrors').DatabaseError;
let UniqueConstraintError: typeof import('./db/errors.db').UniqueConstraintError;
let RepositoryError: typeof import('./db/errors.db').RepositoryError;
let withTransaction: typeof import('./db/index.db').withTransaction;
@@ -106,6 +106,7 @@ describe('AuthService', () => {
const { validatePasswordStrength } = await import('../utils/authUtils');
vi.mocked(validatePasswordStrength).mockReturnValue({ isValid: true, feedback: '' });
sendPasswordResetEmail = (await import('./emailService.server')).sendPasswordResetEmail;
+ DatabaseError = (await import('./processingErrors')).DatabaseError;
UniqueConstraintError = (await import('./db/errors.db')).UniqueConstraintError;
RepositoryError = (await import('./db/errors.db')).RepositoryError;
});
diff --git a/src/services/db/address.db.test.ts b/src/services/db/address.db.test.ts
index f4e4af13..4bf0ce52 100644
--- a/src/services/db/address.db.test.ts
+++ b/src/services/db/address.db.test.ts
@@ -106,7 +106,13 @@ describe('Address DB Service', () => {
'An identical address already exists.',
);
expect(mockLogger.error).toHaveBeenCalledWith(
- { err: dbError, address: addressData },
+ {
+ err: dbError,
+ address: addressData,
+ code: '23505',
+ constraint: undefined,
+ detail: undefined,
+ },
'Database error in upsertAddress',
);
});
diff --git a/src/services/flyerProcessingService.server.ts b/src/services/flyerProcessingService.server.ts
index 6ee92dd7..a21adb96 100644
--- a/src/services/flyerProcessingService.server.ts
+++ b/src/services/flyerProcessingService.server.ts
@@ -108,25 +108,32 @@ export class FlyerProcessingService {
stages[3].status = 'in-progress';
await job.updateProgress({ stages });
- const { flyer } = await db.withTransaction(async (client) => {
- // This assumes createFlyerAndItems is refactored to accept a transactional client.
- const { flyer: newFlyer } = await createFlyerAndItems(flyerData, itemsForDb, logger, client);
+ let flyerId: number;
+ try {
+ const { flyer } = await db.withTransaction(async (client) => {
+ // This assumes createFlyerAndItems is refactored to accept a transactional client.
+ const { flyer: newFlyer } = await createFlyerAndItems(flyerData, itemsForDb, logger, client);
- // Instantiate a new AdminRepository with the transactional client to ensure
- // the activity log is part of the same transaction.
- const transactionalAdminRepo = new AdminRepository(client);
- await transactionalAdminRepo.logActivity(
- {
- action: 'flyer_processed',
- displayText: `Processed flyer for ${flyerData.store_name}`,
- details: { flyer_id: newFlyer.flyer_id, store_name: flyerData.store_name },
- userId: job.data.userId,
- },
- logger,
- );
+ // Instantiate a new AdminRepository with the transactional client to ensure
+ // the activity log is part of the same transaction.
+ const transactionalAdminRepo = new AdminRepository(client);
+ await transactionalAdminRepo.logActivity(
+ {
+ action: 'flyer_processed',
+ displayText: `Processed flyer for ${flyerData.store_name}`,
+ details: { flyer_id: newFlyer.flyer_id, store_name: flyerData.store_name },
+ userId: job.data.userId,
+ },
+ logger,
+ );
- return { flyer: newFlyer };
- });
+ return { flyer: newFlyer };
+ });
+ flyerId = flyer.flyer_id;
+ } catch (error) {
+ if (error instanceof FlyerProcessingError) throw error;
+ throw new DatabaseError(error instanceof Error ? error.message : String(error));
+ }
stages[3].status = 'completed';
await job.updateProgress({ stages });
@@ -134,12 +141,12 @@ export class FlyerProcessingService {
// Enqueue a job to clean up the original and any generated files.
await this.cleanupQueue.add(
'cleanup-flyer-files',
- { flyerId: flyer.flyer_id, paths: allFilePaths },
+ { flyerId, paths: allFilePaths },
{ removeOnComplete: true },
);
- logger.info(`Successfully processed job and enqueued cleanup for flyer ID: ${flyer.flyer_id}`);
+ logger.info(`Successfully processed job and enqueued cleanup for flyer ID: ${flyerId}`);
- return { flyerId: flyer.flyer_id };
+ return { flyerId };
} catch (error) {
logger.warn('Job failed. Temporary files will NOT be cleaned up to allow for manual inspection.');
// Add detailed logging of the raw error object