unit test fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m50s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m50s
This commit is contained in:
@@ -22,7 +22,7 @@ import gamificationRouter from './src/routes/gamification.routes';
|
||||
import systemRouter from './src/routes/system.routes';
|
||||
import healthRouter from './src/routes/health.routes';
|
||||
import { errorHandler } from './src/middleware/errorHandler';
|
||||
import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService.ts';
|
||||
import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService';
|
||||
import { analyticsQueue, weeklyAnalyticsQueue, gracefulShutdown } from './src/services/queueService.server';
|
||||
|
||||
// --- START DEBUG LOGGING ---
|
||||
|
||||
@@ -874,12 +874,12 @@ CREATE INDEX IF NOT EXISTS idx_receipt_items_master_item_id ON public.receipt_it
|
||||
|
||||
-- 54. Store schema metadata to detect changes during deployment.
|
||||
CREATE TABLE IF NOT EXISTS public.schema_info (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
environment TEXT PRIMARY KEY,
|
||||
schema_hash TEXT NOT NULL,
|
||||
deployed_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
CONSTRAINT single_row_check CHECK (id = 1)
|
||||
deployed_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
COMMENT ON TABLE public.schema_info IS 'Stores metadata about the deployed schema, such as a hash of the schema file, to detect changes.';
|
||||
COMMENT ON COLUMN public.schema_info.environment IS 'The deployment environment (e.g., ''development'', ''test'', ''production'').';
|
||||
COMMENT ON COLUMN public.schema_info.schema_hash IS 'A SHA-256 hash of the master_schema_rollup.sql file at the time of deployment.';
|
||||
|
||||
-- 55. Store user reactions to various entities (e.g., recipes, comments).
|
||||
|
||||
@@ -893,12 +893,12 @@ CREATE INDEX IF NOT EXISTS idx_receipt_items_master_item_id ON public.receipt_it
|
||||
|
||||
-- 54. Store schema metadata to detect changes during deployment.
|
||||
CREATE TABLE IF NOT EXISTS public.schema_info (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
environment TEXT PRIMARY KEY,
|
||||
schema_hash TEXT NOT NULL,
|
||||
deployed_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
CONSTRAINT single_row_check CHECK (id = 1)
|
||||
deployed_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
COMMENT ON TABLE public.schema_info IS 'Stores metadata about the deployed schema, such as a hash of the schema file, to detect changes.';
|
||||
COMMENT ON COLUMN public.schema_info.environment IS 'The deployment environment (e.g., ''development'', ''test'', ''production'').';
|
||||
COMMENT ON COLUMN public.schema_info.schema_hash IS 'A SHA-256 hash of the master_schema_rollup.sql file at the time of deployment.';
|
||||
|
||||
-- 55. Store user reactions to various entities (e.g., recipes, comments).
|
||||
@@ -2610,4 +2610,3 @@ BEGIN
|
||||
bp.price_rank = 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ vi.mock('pdfjs-dist', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock the new config module
|
||||
vi.mock('./config', () => ({
|
||||
default: {
|
||||
app: { version: '1.0.0', commitMessage: 'Initial commit', commitUrl: '#' },
|
||||
google: { mapsEmbedApiKey: 'mock-key' },
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedAiApiClient = vi.mocked(aiApiClient); // Mock aiApiClient
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
@@ -369,17 +377,16 @@ describe('App Component', () => {
|
||||
|
||||
describe('Version and "What\'s New" Modal', () => {
|
||||
it('should show the "What\'s New" modal if the app version is new', async () => {
|
||||
// Mock the import.meta.env properties
|
||||
Object.defineProperty(import.meta, 'env', {
|
||||
value: {
|
||||
VITE_APP_VERSION: '1.0.1',
|
||||
VITE_APP_COMMIT_MESSAGE: 'New feature!',
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
// Mock the config module for this specific test
|
||||
vi.mock('./config', () => ({
|
||||
default: {
|
||||
app: { version: '1.0.1', commitMessage: 'New feature!', commitUrl: '#' },
|
||||
google: { mapsEmbedApiKey: 'mock-key' },
|
||||
},
|
||||
}));
|
||||
localStorageMock.setItem('lastSeenVersion', '1.0.0');
|
||||
renderApp();
|
||||
expect(await screen.findByTestId('whats-new-modal-mock')).toBeInTheDocument();
|
||||
await expect(screen.findByTestId('whats-new-modal-mock')).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -458,29 +465,31 @@ describe('App Component', () => {
|
||||
describe('Version Display and What\'s New', () => {
|
||||
const mockEnv = {
|
||||
VITE_APP_VERSION: '2.0.0',
|
||||
VITE_APP_COMMIT_MESSAGE: 'A new version!',
|
||||
VITE_APP_COMMIT_URL: 'http://example.com/commit/2.0.0',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(import.meta, 'env', {
|
||||
value: mockEnv,
|
||||
configurable: true,
|
||||
});
|
||||
// Also mock the config module to reflect this change
|
||||
vi.mock('./config', () => ({
|
||||
default: {
|
||||
app: { version: '2.0.0', commitMessage: 'A new version!', commitUrl: 'http://example.com/commit/2.0.0' },
|
||||
google: { mapsEmbedApiKey: 'mock-key' },
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('should display the version number and commit link', () => {
|
||||
renderApp();
|
||||
const versionLink = screen.getByText(`Version: ${mockEnv.VITE_APP_VERSION}`);
|
||||
const versionLink = screen.getByText(`Version: 2.0.0`);
|
||||
expect(versionLink).toBeInTheDocument();
|
||||
expect(versionLink).toHaveAttribute('href', mockEnv.VITE_APP_COMMIT_URL);
|
||||
expect(versionLink).toHaveAttribute('href', 'http://example.com/commit/2.0.0');
|
||||
});
|
||||
|
||||
it('should open the "What\'s New" modal when the question mark icon is clicked', async () => {
|
||||
renderApp();
|
||||
expect(screen.queryByTestId('whats-new-modal-mock')).not.toBeInTheDocument();
|
||||
|
||||
const openButton = screen.getByTitle("Show what's new in this version");
|
||||
const openButton = await screen.findByTitle("Show what's new in this version");
|
||||
fireEvent.click(openButton);
|
||||
|
||||
expect(await screen.findByTestId('whats-new-modal-mock')).toBeInTheDocument();
|
||||
@@ -527,7 +536,13 @@ describe('App Component', () => {
|
||||
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Profile'));
|
||||
fireEvent.click(await screen.findByText('Login'));
|
||||
const loginButton = await screen.findByText('Login');
|
||||
fireEvent.click(loginButton);
|
||||
|
||||
// We need to wait for the async login function to be called and reject.
|
||||
await waitFor(() => {
|
||||
expect(mockLogin).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import { QuestionMarkCircleIcon } from './components/icons/QuestionMarkCircleIco
|
||||
import { useAuth } from './hooks/useAuth';
|
||||
import { useData } from './hooks/useData';
|
||||
import { MainLayout } from './layouts/MainLayout';
|
||||
import config from './config';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
|
||||
// pdf.js worker configuration
|
||||
@@ -171,9 +172,8 @@ function App() {
|
||||
|
||||
// Read the application version injected at build time.
|
||||
// This will only be available in the production build, not during local development.
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const commitMessage = import.meta.env.VITE_APP_COMMIT_MESSAGE;
|
||||
const commitUrl = import.meta.env.VITE_APP_COMMIT_URL;
|
||||
const appVersion = config.app.version;
|
||||
const commitMessage = config.app.commitMessage;
|
||||
useEffect(() => {
|
||||
if (appVersion) {
|
||||
logger.info(`Application version: ${appVersion}`);
|
||||
@@ -274,7 +274,7 @@ function App() {
|
||||
{appVersion && (
|
||||
<div className="fixed bottom-2 left-3 z-50 flex items-center space-x-2">
|
||||
<a
|
||||
href={commitUrl || '#'}
|
||||
href={config.app.commitUrl || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View commit details on Gitea"
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// src/components/MapView.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { MapView } from './MapView';
|
||||
import config from '../config';
|
||||
|
||||
// Mock the new config module
|
||||
vi.mock('../config', () => ({
|
||||
default: { google: { mapsEmbedApiKey: undefined } },
|
||||
}));
|
||||
|
||||
describe('MapView', () => {
|
||||
const defaultProps = {
|
||||
@@ -10,18 +16,10 @@ describe('MapView', () => {
|
||||
longitude: -74.0060,
|
||||
};
|
||||
|
||||
// Store the original import.meta.env to restore it after tests
|
||||
const originalEnv = import.meta.env;
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment variables to avoid side-effects between tests
|
||||
vi.stubGlobal('import', { meta: { env: originalEnv } });
|
||||
});
|
||||
|
||||
describe('when API key is not configured', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the environment variable to be undefined for this test suite
|
||||
vi.stubGlobal('import', { meta: { env: { VITE_GOOGLE_MAPS_EMBED_API_KEY: undefined } } });
|
||||
// Reset the mock to its default (undefined key)
|
||||
(vi.mocked(config).google.mapsEmbedApiKey as string | undefined) = undefined;
|
||||
});
|
||||
|
||||
it('should render a disabled message', () => {
|
||||
@@ -39,8 +37,8 @@ describe('MapView', () => {
|
||||
const mockApiKey = 'test-api-key';
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the environment variable to have a value for this test suite
|
||||
vi.stubGlobal('import', { meta: { env: { VITE_GOOGLE_MAPS_EMBED_API_KEY: mockApiKey } } });
|
||||
// Set the API key for this test suite
|
||||
(vi.mocked(config).google.mapsEmbedApiKey as string | undefined) = mockApiKey;
|
||||
});
|
||||
|
||||
it('should render the iframe with the correct src URL', () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// src/components/MapView.tsx
|
||||
import React from 'react';
|
||||
import config from '../config';
|
||||
|
||||
interface MapViewProps {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_EMBED_API_KEY;
|
||||
const apiKey = config.google.mapsEmbedApiKey;
|
||||
|
||||
export const MapView: React.FC<MapViewProps> = ({ latitude, longitude }) => {
|
||||
if (!apiKey) {
|
||||
|
||||
19
src/config.ts
Normal file
19
src/config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// src/config.ts
|
||||
|
||||
/**
|
||||
* A centralized configuration module that reads environment variables
|
||||
* from `import.meta.env`. This provides a single, explicit place to manage
|
||||
* environment-specific settings and makes mocking for tests significantly easier.
|
||||
*/
|
||||
const config = {
|
||||
app: {
|
||||
version: import.meta.env.VITE_APP_VERSION,
|
||||
commitMessage: import.meta.env.VITE_APP_COMMIT_MESSAGE,
|
||||
commitUrl: import.meta.env.VITE_APP_COMMIT_URL,
|
||||
},
|
||||
google: {
|
||||
mapsEmbedApiKey: import.meta.env.VITE_GOOGLE_MAPS_EMBED_API_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -75,12 +75,14 @@ describe('BulkImporter', () => {
|
||||
|
||||
it('should call onFilesChange when files are dropped', () => {
|
||||
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
|
||||
const dropzone = screen.getByLabelText(/click to upload/i);
|
||||
// The drop event handlers are on the <label>, not the <input>
|
||||
const dropzone = screen.getByText(/click to upload/i).closest('label')!;
|
||||
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
|
||||
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
files: [file],
|
||||
items: [file], // Also mock the 'items' property for full DataTransfer simulation
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,7 +92,8 @@ describe('BulkImporter', () => {
|
||||
|
||||
it('should not call onFilesChange if no files are dropped', () => {
|
||||
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
|
||||
const dropzone = screen.getByLabelText(/click to upload/i);
|
||||
// The drop event handlers are on the <label>, not the <input>
|
||||
const dropzone = screen.getByText(/click to upload/i).closest('label')!;
|
||||
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
@@ -138,7 +141,8 @@ describe('BulkImporter', () => {
|
||||
|
||||
// Check that a generic document icon is rendered for the PDF
|
||||
const pdfPreviewContainer = screen.getByText('document.pdf').closest('div.relative') as HTMLElement;
|
||||
expect(within(pdfPreviewContainer!).getByTestId('document-text-icon')).toBeInTheDocument();
|
||||
// The icon itself doesn't have a test-id, but we can find it by its role and class.
|
||||
expect(pdfPreviewContainer.querySelector('svg.w-12.h-12')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow removing a file from the preview list', async () => {
|
||||
|
||||
@@ -43,10 +43,7 @@ const renderComponent = (onProcessingComplete = vi.fn()) => {
|
||||
};
|
||||
|
||||
describe('FlyerUploader', { timeout: 20000 }, () => {
|
||||
beforeEach(() => {
|
||||
// FIX: Do not enable fake timers globally. It causes waitFor to hang in non-polling tests.
|
||||
// vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
beforeEach(() => { vi.clearAllMocks();
|
||||
|
||||
// Access the mock implementation directly from the mocked module.
|
||||
mockedChecksumModule.generateFileChecksum.mockResolvedValue('mock-checksum');
|
||||
@@ -57,7 +54,7 @@ describe('FlyerUploader', { timeout: 20000 }, () => {
|
||||
|
||||
afterEach(() => {
|
||||
// Restore real timers after each test to avoid side effects.
|
||||
vi.useRealTimers();
|
||||
vi.useRealTimers(); // Ensure timers are reset after each test
|
||||
});
|
||||
|
||||
it('should render the initial state correctly', () => {
|
||||
@@ -139,6 +136,9 @@ describe('FlyerUploader', { timeout: 20000 }, () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/flyers/42');
|
||||
|
||||
// Clean up fake timers for this test
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle a failed job', async () => {
|
||||
@@ -218,5 +218,8 @@ describe('FlyerUploader', { timeout: 20000 }, () => {
|
||||
expect(screen.getByText('Select a flyer (PDF or image) to begin.')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Stop Watching Progress' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Clean up fake timers for this test
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -47,28 +47,28 @@ describe('ProcessingStatus', () => {
|
||||
|
||||
// Completed stage
|
||||
const completedStageText = screen.getByTestId('stage-text-0');
|
||||
expect(completedStageText).toHaveClass('text-gray-700');
|
||||
expect(completedStageText.className).toContain('text-gray-700');
|
||||
expect(screen.getByTestId('stage-icon-0').querySelector('svg')).toHaveClass('text-green-500');
|
||||
|
||||
// In-progress stage`
|
||||
const inProgressStageText = screen.getByTestId('stage-text-1');
|
||||
expect(inProgressStageText).toHaveClass('text-brand-primary');
|
||||
expect(inProgressStageText.className).toContain('text-brand-primary');
|
||||
expect(screen.getByTestId('stage-icon-1').querySelector('svg')).toHaveClass('animate-spin');
|
||||
|
||||
// Pending stage
|
||||
const pendingStageText = screen.getByTestId('stage-text-2');
|
||||
expect(pendingStageText).toHaveClass('text-gray-400');
|
||||
expect(pendingStageText.className).toContain('text-gray-400');
|
||||
expect(screen.getByTestId('stage-icon-2').querySelector('div')).toHaveClass('border-gray-400');
|
||||
|
||||
// Non-critical error stage
|
||||
const nonCriticalErrorStageText = screen.getByTestId('stage-text-3');
|
||||
expect(nonCriticalErrorStageText).toHaveClass('text-yellow-600');
|
||||
expect(nonCriticalErrorStageText.className).toContain('text-yellow-600');
|
||||
expect(screen.getByTestId('stage-icon-3').querySelector('svg')).toHaveClass('text-yellow-500');
|
||||
expect(screen.getByText(/optional/i)).toBeInTheDocument();
|
||||
|
||||
// Critical error stage
|
||||
const criticalErrorStageText = screen.getByTestId('stage-text-4');
|
||||
expect(criticalErrorStageText).toHaveClass('text-red-500');
|
||||
expect(criticalErrorStageText.className).toContain('text-red-500');
|
||||
expect(screen.getByTestId('stage-icon-4').querySelector('svg')).toHaveClass('text-red-500');
|
||||
});
|
||||
|
||||
|
||||
@@ -121,10 +121,12 @@ describe('useAuth Hook and AuthProvider', () => {
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAuth(), { wrapper });
|
||||
// Wait for initial check to complete
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
|
||||
await act(async () => {
|
||||
// Wait for the initial "Determining..." state to pass
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
await result.current.login(mockUser, 'new-valid-token');
|
||||
});
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('MainLayout Component', () => {
|
||||
updateProfile: vi.fn(),
|
||||
});
|
||||
mockedUseData.mockReturnValue({
|
||||
flyers: [{ flyer_id: 1, file_name: 'flyer.jpg' } as any],
|
||||
flyers: [{ flyer_id: 1, file_name: 'flyer.jpg' }],
|
||||
masterItems: [],
|
||||
watchedItems: [],
|
||||
shoppingLists: [],
|
||||
@@ -84,7 +84,7 @@ describe('MainLayout Component', () => {
|
||||
refetchFlyers: vi.fn(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} as any);
|
||||
mockedUseShoppingLists.mockReturnValue({
|
||||
shoppingLists: [],
|
||||
activeListId: null,
|
||||
@@ -95,19 +95,19 @@ describe('MainLayout Component', () => {
|
||||
updateItemInList: vi.fn(),
|
||||
removeItemFromList: vi.fn(),
|
||||
error: null,
|
||||
});
|
||||
} as any);
|
||||
mockedUseWatchedItems.mockReturnValue({
|
||||
watchedItems: [],
|
||||
addWatchedItem: vi.fn(),
|
||||
removeWatchedItem: vi.fn(),
|
||||
error: null,
|
||||
});
|
||||
} as any);
|
||||
mockedUseActiveDeals.mockReturnValue({
|
||||
activeDeals: [],
|
||||
totalActiveItems: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
error: null
|
||||
} as any);
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
@@ -137,7 +137,7 @@ describe('MainLayout Component', () => {
|
||||
});
|
||||
|
||||
it('does not show the AnonymousUserBanner if there are no flyers', () => {
|
||||
mockedUseData.mockReturnValueOnce({ ...mockedUseData(), flyers: [] } as any);
|
||||
mockedUseData.mockReturnValueOnce({ ...mockedUseData.mock.results[0].value, flyers: [] });
|
||||
renderWithRouter(<MainLayout {...defaultProps} />);
|
||||
expect(screen.queryByTestId('anonymous-banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -180,16 +180,15 @@ describe('ProfileManager Authentication Flows', () => {
|
||||
|
||||
// 2. Fill out all fields in the form
|
||||
fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'New Test User' } });
|
||||
fireEvent.change(screen.getByLabelText(/avatar url/i), { target: { value: 'http://example.com/new.png' } });
|
||||
fireEvent.change(screen.getByLabelText(/^Email Address$/i), { target: { value: 'newuser@test.com' } });
|
||||
fireEvent.change(screen.getByLabelText(/^Password$/i), { target: { value: 'newsecurepassword' } });
|
||||
|
||||
// 3. Submit the registration form
|
||||
fireEvent.submit(screen.getByTestId('auth-form'));
|
||||
|
||||
// 4. Assert that the correct functions were called with the correct data
|
||||
// 4. Assert that the correct functions were called with the correct data (without avatar_url)
|
||||
await waitFor(() => {
|
||||
expect(mockedApiClient.registerUser).toHaveBeenCalledWith('newuser@test.com', 'newsecurepassword', 'New Test User', 'http://example.com/new.png');
|
||||
expect(mockedApiClient.registerUser).toHaveBeenCalledWith('newuser@test.com', 'newsecurepassword', 'New Test User', '');
|
||||
expect(mockOnLoginSuccess).toHaveBeenCalledWith(
|
||||
{ user_id: '123', email: 'test@example.com' },
|
||||
'mock-token',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/routes/user.routes.ts
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import passport from './passport.routes.ts';
|
||||
import passport from './passport.routes';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
@@ -72,10 +72,9 @@ describe('Budget DB Service', () => {
|
||||
|
||||
// Mock the sequence of queries within the transaction
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // INSERT...RETURNING
|
||||
.mockResolvedValueOnce({ rows: [] }) // award_achievement
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
.mockResolvedValueOnce({ rows: [] }) // For BEGIN
|
||||
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // For the INSERT...RETURNING
|
||||
.mockResolvedValueOnce({ rows: [] }); // For both award_achievement and COMMIT
|
||||
|
||||
const result = await budgetRepo.createBudget('user-123', budgetData);
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ describe('Flyer DB Service', () => {
|
||||
// Mock the sequence of calls within the transaction
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // findOrCreateStore
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
|
||||
.mockResolvedValueOnce({ rows: mockItems }) // insertFlyerItems
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
|
||||
@@ -338,11 +338,10 @@ describe('Personalization DB Service', () => {
|
||||
expect(mockQuery).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockQuery).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
|
||||
|
||||
// FIX: Make assertion robust for array parameters
|
||||
expect(mockQuery).toHaveBeenNthCalledWith(3,
|
||||
// The implementation uses unnest, so it's one call with an array parameter
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO public.user_dietary_restrictions'),
|
||||
['user-123', [1, 2]]
|
||||
);
|
||||
['user-123', [1, 2]]);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
|
||||
@@ -196,7 +196,10 @@ describe('Recipe DB Service', () => {
|
||||
const updates = { name: 'Updated Recipe', description: 'New desc' };
|
||||
const result = await recipeRepo.updateRecipe(1, 'user-123', updates);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.recipes'), ['Updated Recipe', 'New desc', 1, 'user-123']);
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE public.recipes'),
|
||||
['Updated Recipe', 'New desc', 1, 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockRecipe);
|
||||
});
|
||||
|
||||
|
||||
@@ -423,7 +423,6 @@ describe('Shopping DB Service', () => {
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
mockClient.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }];
|
||||
|
||||
await shoppingRepo.processReceiptItems(1, items);
|
||||
@@ -431,14 +430,7 @@ describe('Shopping DB Service', () => {
|
||||
const expectedItemsWithQuantity = [{ raw_item_description: 'Milk', price_paid_cents: 399, quantity: 1 }];
|
||||
expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockClient.query).toHaveBeenCalledWith(
|
||||
'SELECT public.process_receipt_items($1, $2, $3)',
|
||||
[
|
||||
1,
|
||||
// The order of keys in the stringified JSON is not guaranteed.
|
||||
// Instead, we'll parse the JSON string from the mock call and check its contents.
|
||||
expect.stringContaining('"raw_item_description":"Milk"'),
|
||||
expect.stringContaining('"raw_item_description":"Milk"'),
|
||||
]
|
||||
'SELECT public.process_receipt_items($1, $2, $3)', [1, JSON.stringify(expectedItemsWithQuantity), JSON.stringify(expectedItemsWithQuantity)]
|
||||
);
|
||||
expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
@@ -452,14 +444,7 @@ describe('Shopping DB Service', () => {
|
||||
const items = [{ raw_item_description: 'Bag', price_paid_cents: 0 }];
|
||||
await shoppingRepo.processReceiptItems(1, items);
|
||||
const expectedItems = [{ raw_item_description: 'Bag', price_paid_cents: 0, quantity: 1 }];
|
||||
const call = mockClient.query.mock.calls.find(c => c[0].includes('process_receipt_items'));
|
||||
expect(call).toBeDefined();
|
||||
const passedJson = JSON.parse(call![1]);
|
||||
expect(mockClient.query).toHaveBeenCalledWith(
|
||||
'SELECT public.process_receipt_items($1, $2, $3)',
|
||||
[1, expect.any(String), expect.any(String)]
|
||||
);
|
||||
expect(passedJson).toEqual(expect.arrayContaining([expect.objectContaining(expectedItems[0])]));
|
||||
expect(mockClient.query).toHaveBeenCalledWith('SELECT public.process_receipt_items($1, $2, $3)', [1, JSON.stringify(expectedItems), JSON.stringify(expectedItems)]);
|
||||
});
|
||||
|
||||
it('should update receipt status to "failed" on error', async () => {
|
||||
@@ -474,7 +459,7 @@ describe('Shopping DB Service', () => {
|
||||
await expect(shoppingRepo.processReceiptItems(1, items)).rejects.toThrow('Failed to process and save receipt items.');
|
||||
|
||||
// Verify that the status was updated to 'failed' in the catch block
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith("UPDATE public.receipts SET status = 'failed' WHERE id = $1", [1]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith("UPDATE public.receipts SET status = 'failed' WHERE receipt_id = $1", [1]);
|
||||
});
|
||||
|
||||
// Note: The `processReceiptItems` method in shopping.db.ts has a potential bug where it calls `client.query('ROLLBACK')`
|
||||
|
||||
@@ -94,10 +94,9 @@ describe('User DB Service', () => {
|
||||
// Mock the sequence of queries within the transaction
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockResolvedValueOnce({ rows: [mockProfile] }) // SELECT profile
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
.mockResolvedValueOnce({ rows: [] }) // COMMIT;
|
||||
|
||||
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' });
|
||||
|
||||
@@ -113,13 +112,11 @@ describe('User DB Service', () => {
|
||||
|
||||
// Arrange: Mock the user insert query to fail after BEGIN and set_config
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockRejectedValueOnce(new Error('User insert failed')); // INSERT fails
|
||||
|
||||
// Act & Assert
|
||||
// FIX: The repo now throws "Failed to create user in database." instead of the original error.
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {})).rejects.toThrow('User insert failed');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('BEGIN'); // This will be called inside the try block
|
||||
// The createUser function now throws the original error, so we check for that.
|
||||
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
@@ -132,13 +129,10 @@ describe('User DB Service', () => {
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [] }) // set_config in trigger
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockRejectedValueOnce(new Error('Profile fetch failed')); // SELECT profile fails
|
||||
|
||||
// FIX: The repo wraps the error, so expect the wrapped message or generic failure.
|
||||
// Based on implementation: "Failed to create user in database."
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {})).rejects.toThrow('Profile fetch failed');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
});
|
||||
@@ -157,7 +151,6 @@ describe('User DB Service', () => {
|
||||
// 4. ROLLBACK (success)
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||
.mockRejectedValueOnce(dbError) // INSERT fails
|
||||
.mockResolvedValueOnce({ rows: [] }); // ROLLBACK
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// src/utils/checksum.test.ts
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { generateFileChecksum } from './checksum';
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// src/utils/pdfConverter.test.ts
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { convertPdfToImageFiles } from './pdfConverter';
|
||||
|
||||
|
||||
13
src/vite-env.d.ts
vendored
Normal file
13
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/// <reference types="vite/client" />
|
||||
// src/vite-env.d.ts
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_VERSION: string;
|
||||
readonly VITE_APP_COMMIT_MESSAGE: string;
|
||||
readonly VITE_APP_COMMIT_URL: string;
|
||||
readonly VITE_GOOGLE_MAPS_EMBED_API_KEY: string;
|
||||
// Add any other environment variables you use here
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -1,35 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
"vite/client", // Add this line to include Vite's client-side types
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true, // Often helpful for broader library compatibility
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"strict": true, // Enforcing strict mode is a best practice for new projects.
|
||||
"forceConsistentCasingInFileNames": true // Helps prevent casing-related import errors.
|
||||
"jsx": "react-jsx",
|
||||
// This line makes Vitest's global APIs (describe, it, expect) available everywhere
|
||||
// without needing to import them.
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": ["src", ".vitepress", "vite.config.ts", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", "dist", "coverage"]
|
||||
// This is the most important part: It tells TypeScript to include ALL files
|
||||
// within the 'src' directory, including your new 'vite-env.d.ts' file.
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
13
tsconfig.node.json
Normal file
13
tsconfig.node.json
Normal file
@@ -0,0 +1,13 @@
|
||||
// tsconfig.node.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler", // Changed from "Node"
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true, // It's good practice to keep tooling config strict
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts", "vitest.config.ts", "vitest.config.integration.ts", "vitest.workspace.ts"]
|
||||
}
|
||||
@@ -38,10 +38,12 @@ export default defineConfig({
|
||||
// Disable file parallelism to run tests sequentially (replaces --no-threads)
|
||||
fileParallelism: false,
|
||||
environment: 'jsdom',
|
||||
// Explicitly point Vitest to the correct tsconfig and enable globals.
|
||||
globals: true, // tsconfig is auto-detected, so the explicit property is not needed and causes an error.
|
||||
globalSetup: './src/tests/setup/global-setup.ts',
|
||||
setupFiles: ['./src/tests/setup/tests-setup-unit.ts'],
|
||||
// Explicitly include all test files that are NOT integration tests.
|
||||
include: ['src/**/*.test.{ts,tsx}'],
|
||||
include: ['src/**/*.test.{ts,tsx}', 'src/vite-env.d.ts'],
|
||||
// Exclude integration tests and other non-test files from the unit test runner.
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
|
||||
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
// This setup file is where we can add global test configurations
|
||||
setupFiles: './src/tests/setup/tests-setup-unit.ts',
|
||||
// This line is the key fix: it tells Vitest to include the type definitions
|
||||
include: ['src/**/*.test.tsx', 'src/vite-env.d.ts'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user