brand new unit tests finally
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 2m12s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 2m12s
This commit is contained in:
142
src/services/db/admin.test.ts
Normal file
142
src/services/db/admin.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// src/services/db/admin.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPool } from './connection';
|
||||
import {
|
||||
getSuggestedCorrections,
|
||||
approveCorrection,
|
||||
rejectCorrection,
|
||||
updateSuggestedCorrection,
|
||||
getApplicationStats,
|
||||
getDailyStatsForLast30Days,
|
||||
logActivity,
|
||||
incrementFailedLoginAttempts,
|
||||
resetFailedLoginAttempts,
|
||||
updateBrandLogo,
|
||||
} from './admin';
|
||||
import type { SuggestedCorrection } from '../../types';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
// This allows us to control the behavior of database queries for all tests in this file.
|
||||
const mockQuery = vi.fn();
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Admin DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// Clear mock history before each test
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getSuggestedCorrections', () => {
|
||||
it('should execute the correct query and return corrections', async () => {
|
||||
const mockCorrections: SuggestedCorrection[] = [
|
||||
{ suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '250', status: 'pending', created_at: new Date().toISOString() },
|
||||
];
|
||||
mockQuery.mockResolvedValue({ rows: mockCorrections });
|
||||
|
||||
const result = await getSuggestedCorrections();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining("FROM public.suggested_corrections sc"));
|
||||
expect(result).toEqual(mockCorrections);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approveCorrection', () => {
|
||||
it('should call the approve_correction database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] }); // Mock the function call
|
||||
await approveCorrection(123);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT public.approve_correction($1)', [123]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectCorrection', () => {
|
||||
it('should update the correction status to rejected', async () => {
|
||||
mockQuery.mockResolvedValue({ rowCount: 1 });
|
||||
await rejectCorrection(123);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE public.suggested_corrections SET status = 'rejected'"),
|
||||
[123]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSuggestedCorrection', () => {
|
||||
it('should update the suggested value and return the updated correction', async () => {
|
||||
const mockCorrection: SuggestedCorrection = { suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '300', status: 'pending', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockCorrection], rowCount: 1 });
|
||||
|
||||
const result = await updateSuggestedCorrection(1, '300');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE public.suggested_corrections SET suggested_value = $1"),
|
||||
['300', 1]
|
||||
);
|
||||
expect(result).toEqual(mockCorrection);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApplicationStats', () => {
|
||||
it('should execute 5 parallel count queries and return the aggregated stats', async () => {
|
||||
// Mock responses for each of the 5 parallel queries
|
||||
mockQuery
|
||||
.mockResolvedValueOnce({ rows: [{ count: '10' }] }) // flyerCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '20' }] }) // userCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '300' }] }) // flyerItemCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '5' }] }) // storeCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '2' }] }); // pendingCorrectionCount
|
||||
|
||||
const stats = await getApplicationStats();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledTimes(5);
|
||||
expect(stats).toEqual({
|
||||
flyerCount: 10,
|
||||
userCount: 20,
|
||||
flyerItemCount: 300,
|
||||
storeCount: 5,
|
||||
pendingCorrectionCount: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDailyStatsForLast30Days', () => {
|
||||
it('should execute the correct query to get daily stats', async () => {
|
||||
const mockStats = [{ date: '2023-01-01', new_users: 5, new_flyers: 2 }];
|
||||
mockQuery.mockResolvedValue({ rows: mockStats });
|
||||
|
||||
const result = await getDailyStatsForLast30Days();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining("WITH date_series AS"));
|
||||
expect(result).toEqual(mockStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logActivity', () => {
|
||||
it('should insert a new activity log entry', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
const logData = { userId: 'user-123', action: 'test_action', displayText: 'Test activity' };
|
||||
await logActivity(logData);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("INSERT INTO public.activity_log"),
|
||||
[logData.userId, logData.action, logData.displayText, null, null]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// (Additional tests for other functions like updateBrandLogo, etc., would follow the same pattern)
|
||||
});
|
||||
90
src/services/db/connection.test.ts
Normal file
90
src/services/db/connection.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// src/services/db/connection.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
// Mock the 'pg' module BEFORE importing the connection service
|
||||
const mockQuery = vi.fn();
|
||||
const mockPoolInstance = {
|
||||
query: mockQuery,
|
||||
totalCount: 10,
|
||||
idleCount: 5,
|
||||
waitingCount: 0,
|
||||
poolId: 'mock-pool-id',
|
||||
};
|
||||
|
||||
vi.mock('pg', () => {
|
||||
// Mock the Pool class constructor
|
||||
const Pool = vi.fn(() => mockPoolInstance);
|
||||
return { Pool };
|
||||
});
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DB Connection Service', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// This is important to reset the singleton for each test
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe('getPool', () => {
|
||||
it('should create a new pool instance on the first call', async () => {
|
||||
// Dynamically import to get the fresh, un-cached module
|
||||
const { getPool } = await import('./connection');
|
||||
const pool = getPool();
|
||||
|
||||
expect(Pool).toHaveBeenCalledTimes(1);
|
||||
expect(pool).toBe(mockPoolInstance);
|
||||
});
|
||||
|
||||
it('should return the same pool instance on subsequent calls', async () => {
|
||||
const { getPool } = await import('./connection');
|
||||
const pool1 = getPool();
|
||||
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(pool1).toBe(pool2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkTablesExist', () => {
|
||||
it('should return an empty array if all tables exist', async () => {
|
||||
const { checkTablesExist } = await import('./connection');
|
||||
const tableNames = ['users', 'flyers'];
|
||||
mockQuery.mockResolvedValue({ rows: [{ table_name: 'users' }, { table_name: 'flyers' }] });
|
||||
|
||||
const missingTables = await checkTablesExist(tableNames);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.any(String), [tableNames]);
|
||||
expect(missingTables).toEqual([]);
|
||||
});
|
||||
|
||||
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 missingTables = await checkTablesExist(tableNames);
|
||||
|
||||
expect(missingTables).toEqual(['flyers', 'products']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPoolStatus', () => {
|
||||
it('should return the status counts from the pool instance', async () => {
|
||||
const { getPoolStatus } = await import('./connection');
|
||||
const status = getPoolStatus();
|
||||
|
||||
expect(status).toEqual({ totalCount: 10, idleCount: 5, waitingCount: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
171
src/services/db/flyer.test.ts
Normal file
171
src/services/db/flyer.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// src/services/db/flyer.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPool } from './connection';
|
||||
import {
|
||||
getFlyers,
|
||||
getAllBrands,
|
||||
getAllMasterItems,
|
||||
getAllCategories,
|
||||
findFlyerByChecksum,
|
||||
createFlyerAndItems,
|
||||
getFlyerItems,
|
||||
getFlyerItemsForFlyers,
|
||||
countFlyerItemsForFlyers,
|
||||
updateStoreLogo,
|
||||
trackFlyerItemInteraction,
|
||||
getHistoricalPriceDataForItems,
|
||||
} from './flyer';
|
||||
import type { Flyer, FlyerItem } from '../../types';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
const mockQuery = vi.fn();
|
||||
const mockConnect = vi.fn(() => ({
|
||||
query: mockQuery,
|
||||
release: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
connect: mockConnect,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Flyer DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getFlyers', () => {
|
||||
it('should execute the correct query and return flyers', async () => {
|
||||
const mockFlyers: Flyer[] = [{ flyer_id: 1, file_name: 'test.jpg', image_url: 'url', created_at: new Date().toISOString() }];
|
||||
mockQuery.mockResolvedValue({ rows: mockFlyers });
|
||||
|
||||
const result = await getFlyers();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.flyers f'));
|
||||
expect(result).toEqual(mockFlyers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllBrands', () => {
|
||||
it('should execute the correct query to fetch all brands', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ brand_id: 1, name: 'Test Brand' }] });
|
||||
const result = await getAllBrands();
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.brands b'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('Test Brand');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllMasterItems', () => {
|
||||
it('should execute the correct query to fetch all master items', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ master_grocery_item_id: 1, name: 'Test Item' }] });
|
||||
const result = await getAllMasterItems();
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.master_grocery_items m'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('Test Item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllCategories', () => {
|
||||
it('should execute the correct query to fetch all categories', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ category_id: 1, name: 'Test Category' }] });
|
||||
const result = await getAllCategories();
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.categories'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('Test Category');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findFlyerByChecksum', () => {
|
||||
it('should query for a flyer by its checksum', async () => {
|
||||
const checksum = 'test-checksum';
|
||||
mockQuery.mockResolvedValue({ rows: [{ flyer_id: 1, checksum }] });
|
||||
const result = await findFlyerByChecksum(checksum);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.flyers WHERE checksum = $1', [checksum]);
|
||||
expect(result?.flyer_id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFlyerAndItems', () => {
|
||||
it('should execute a transaction to create a flyer and its items', async () => {
|
||||
const flyerData = { file_name: 'flyer.jpg', image_url: '/img.jpg', checksum: 'cs', store_name: 'Test Store', valid_from: null, valid_to: null, store_address: null, uploaded_by: null };
|
||||
const items: Omit<FlyerItem, 'flyer_item_id' | 'flyer_id' | 'created_at'>[] = [{ item: 'Test Item', price_display: '$1', price_in_cents: 100, quantity: 'each', view_count: 0, click_count: 0, updated_at: '' }];
|
||||
|
||||
// Mock the sequence of queries in the transaction
|
||||
mockQuery
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // Find store
|
||||
.mockResolvedValueOnce({ rows: [{ flyer_id: 123, ...flyerData }] }) // Insert flyer
|
||||
.mockResolvedValueOnce({ rows: [] }); // Insert flyer item
|
||||
|
||||
await createFlyerAndItems(flyerData, items);
|
||||
|
||||
expect(mockConnect).toHaveBeenCalled();
|
||||
expect(mockQuery).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.flyers'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.flyer_items'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFlyerItems', () => {
|
||||
it('should query for items by a specific flyer ID', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ item: 'Test Item' }] });
|
||||
const result = await getFlyerItems(123);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('WHERE flyer_id = $1'), [123]);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFlyerItemsForFlyers', () => {
|
||||
it('should query for items from a list of flyer IDs', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getFlyerItemsForFlyers([1, 2, 3]);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('WHERE flyer_id = ANY($1::bigint[])'), [[1, 2, 3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countFlyerItemsForFlyers', () => {
|
||||
it('should query for a count of items from a list of flyer IDs', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ count: '42' }] });
|
||||
const result = await countFlyerItemsForFlyers([1, 2, 3]);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('SELECT COUNT(*) FROM public.flyer_items'), [[1, 2, 3]]);
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStoreLogo', () => {
|
||||
it('should execute an UPDATE query for the store logo', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await updateStoreLogo(1, '/logo.png');
|
||||
expect(getPool().query).toHaveBeenCalledWith('UPDATE public.stores SET logo_url = $1 WHERE id = $2', ['/logo.png', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackFlyerItemInteraction', () => {
|
||||
it('should execute an UPDATE query to increment the view_count', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await trackFlyerItemInteraction(101, 'view');
|
||||
expect(getPool().query).toHaveBeenCalledWith('UPDATE public.flyer_items SET view_count = view_count + 1 WHERE flyer_item_id = $1', [101]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoricalPriceDataForItems', () => {
|
||||
it('should query the item_price_history table with a list of IDs', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getHistoricalPriceDataForItems([1, 2, 3]);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.item_price_history'), [[1, 2, 3]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
195
src/services/db/personalization.test.ts
Normal file
195
src/services/db/personalization.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// src/services/db/personalization.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPool } from './connection';
|
||||
import {
|
||||
getWatchedItems,
|
||||
addWatchedItem,
|
||||
removeWatchedItem,
|
||||
findRecipesFromPantry,
|
||||
recommendRecipesForUser,
|
||||
getBestSalePricesForUser,
|
||||
suggestPantryItemConversions,
|
||||
findPantryItemOwner,
|
||||
getDietaryRestrictions,
|
||||
getUserDietaryRestrictions,
|
||||
setUserDietaryRestrictions,
|
||||
getAppliances,
|
||||
getUserAppliances,
|
||||
setUserAppliances,
|
||||
getRecipesForUserDiets,
|
||||
} from './personalization';
|
||||
import type { MasterGroceryItem, PantryRecipe, RecommendedRecipe, WatchedItemDeal, PantryItemConversion, DietaryRestriction, Appliance, UserAppliance } from '../../types';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
const mockQuery = vi.fn();
|
||||
const mockConnect = vi.fn(() => ({
|
||||
query: mockQuery,
|
||||
release: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
connect: mockConnect,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Personalization DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getWatchedItems', () => {
|
||||
it('should execute the correct query and return watched items', async () => {
|
||||
const mockItems: MasterGroceryItem[] = [{ master_grocery_item_id: 1, name: 'Apples', created_at: '' }];
|
||||
mockQuery.mockResolvedValue({ rows: mockItems });
|
||||
|
||||
const result = await getWatchedItems('user-123');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.master_grocery_items mgi'), ['user-123']);
|
||||
expect(result).toEqual(mockItems);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addWatchedItem', () => {
|
||||
it('should execute a transaction to add a watched item', async () => {
|
||||
const mockItem: MasterGroceryItem = { master_grocery_item_id: 1, name: 'New Item', created_at: '' };
|
||||
mockQuery
|
||||
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
|
||||
.mockResolvedValueOnce({ rows: [mockItem] }) // Find/create master item
|
||||
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
|
||||
|
||||
await addWatchedItem('user-123', 'New Item', 'Produce');
|
||||
|
||||
expect(mockConnect).toHaveBeenCalled();
|
||||
expect(mockQuery).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('SELECT category_id FROM public.categories'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('SELECT * FROM public.master_grocery_items'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_watched_items'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWatchedItem', () => {
|
||||
it('should execute a DELETE query', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await removeWatchedItem('user-123', 1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('DELETE FROM public.user_watched_items WHERE user_id = $1 AND master_item_id = $2', ['user-123', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRecipesFromPantry', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await findRecipesFromPantry('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.find_recipes_from_pantry($1)', ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recommendRecipesForUser', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await recommendRecipesForUser('user-123', 5);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.recommend_recipes_for_user($1, $2)', ['user-123', 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBestSalePricesForUser', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getBestSalePricesForUser('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.get_best_sale_prices_for_user($1)', ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('suggestPantryItemConversions', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await suggestPantryItemConversions(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.suggest_pantry_item_conversions($1)', [1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPantryItemOwner', () => {
|
||||
it('should execute a SELECT query to find the owner', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ user_id: 'user-123' }] });
|
||||
const result = await findPantryItemOwner(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT user_id FROM public.pantry_items WHERE pantry_item_id = $1', [1]);
|
||||
expect(result?.user_id).toBe('user-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDietaryRestrictions', () => {
|
||||
it('should execute a SELECT query to get all restrictions', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getDietaryRestrictions();
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.dietary_restrictions ORDER BY type, name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDietaryRestrictions', () => {
|
||||
it('should execute a SELECT query with a JOIN', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getUserDietaryRestrictions('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.dietary_restrictions dr'), ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUserDietaryRestrictions', () => {
|
||||
it('should execute a transaction to set restrictions', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await setUserDietaryRestrictions('user-123', [1, 2]);
|
||||
expect(mockConnect).toHaveBeenCalled();
|
||||
expect(mockQuery).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockQuery).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_dietary_restrictions'));
|
||||
expect(mockQuery).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAppliances', () => {
|
||||
it('should execute a SELECT query to get all appliances', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getAppliances();
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.appliances ORDER BY name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAppliances', () => {
|
||||
it('should execute a SELECT query with a JOIN', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getUserAppliances('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.appliances a'), ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUserAppliances', () => {
|
||||
it('should execute a transaction to set appliances', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await setUserAppliances('user-123', [1, 2]);
|
||||
expect(mockConnect).toHaveBeenCalled();
|
||||
expect(mockQuery).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockQuery).toHaveBeenCalledWith('DELETE FROM public.user_appliances WHERE user_id = $1', ['user-123']);
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_appliances'), expect.any(Array));
|
||||
expect(mockQuery).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecipesForUserDiets', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipesForUserDiets('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.get_recipes_for_user_diets($1)', ['user-123']);
|
||||
});
|
||||
});
|
||||
});
|
||||
122
src/services/db/recipe.test.ts
Normal file
122
src/services/db/recipe.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// src/services/db/recipe.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPool } from './connection';
|
||||
import {
|
||||
getRecipesBySalePercentage,
|
||||
getRecipesByMinSaleIngredients,
|
||||
findRecipesByIngredientAndTag,
|
||||
getUserFavoriteRecipes,
|
||||
addFavoriteRecipe,
|
||||
removeFavoriteRecipe,
|
||||
getRecipeComments,
|
||||
addRecipeComment,
|
||||
forkRecipe,
|
||||
} from './recipe';
|
||||
import type { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
const mockQuery = vi.fn();
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Recipe DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getRecipesBySalePercentage', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipesBySalePercentage(50);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.get_recipes_by_sale_percentage($1)', [50]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecipesByMinSaleIngredients', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipesByMinSaleIngredients(3);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.get_recipes_by_min_sale_ingredients($1)', [3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRecipesByIngredientAndTag', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await findRecipesByIngredientAndTag('chicken', 'quick');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.find_recipes_by_ingredient_and_tag($1, $2)', ['chicken', 'quick']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFavoriteRecipes', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getUserFavoriteRecipes('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.get_user_favorite_recipes($1)', ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFavoriteRecipe', () => {
|
||||
it('should execute an INSERT query and return the new favorite', async () => {
|
||||
const mockFavorite: FavoriteRecipe = { user_id: 'user-123', recipe_id: 1, created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockFavorite] });
|
||||
|
||||
const result = await addFavoriteRecipe('user-123', 1);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.favorite_recipes'), ['user-123', 1]);
|
||||
expect(result).toEqual(mockFavorite);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFavoriteRecipe', () => {
|
||||
it('should execute a DELETE query', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await removeFavoriteRecipe('user-123', 1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('DELETE FROM public.favorite_recipes WHERE user_id = $1 AND recipe_id = $2', ['user-123', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecipeComments', () => {
|
||||
it('should execute a SELECT query with a JOIN', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipeComments(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.recipe_comments rc'), [1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRecipeComment', () => {
|
||||
it('should execute an INSERT query and return the new comment', async () => {
|
||||
const mockComment: RecipeComment = { recipe_comment_id: 1, recipe_id: 1, user_id: 'user-123', content: 'Great!', status: 'visible', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockComment] });
|
||||
|
||||
const result = await addRecipeComment(1, 'user-123', 'Great!');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.recipe_comments'), [1, 'user-123', 'Great!', undefined]);
|
||||
expect(result).toEqual(mockComment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forkRecipe', () => {
|
||||
it('should call the fork_recipe database function', async () => {
|
||||
const mockRecipe: Recipe = { recipe_id: 2, name: 'Forked Recipe', avg_rating: 0, rating_count: 0, status: 'private', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockRecipe] });
|
||||
|
||||
const result = await forkRecipe('user-123', 1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.fork_recipe($1, $2)', ['user-123', 1]);
|
||||
expect(result).toEqual(mockRecipe);
|
||||
});
|
||||
});
|
||||
});
|
||||
195
src/services/db/shopping.test.ts
Normal file
195
src/services/db/shopping.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// src/services/db/shopping.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPool } from './connection';
|
||||
import {
|
||||
getShoppingLists,
|
||||
createShoppingList,
|
||||
deleteShoppingList,
|
||||
addShoppingListItem,
|
||||
removeShoppingListItem,
|
||||
generateShoppingListForMenuPlan,
|
||||
addMenuPlanToShoppingList,
|
||||
getPantryLocations,
|
||||
createPantryLocation,
|
||||
updateShoppingListItem,
|
||||
completeShoppingList,
|
||||
getShoppingTripHistory,
|
||||
createReceipt,
|
||||
processReceiptItems,
|
||||
findDealsForReceipt,
|
||||
findReceiptOwner,
|
||||
} from './shopping';
|
||||
import type { ShoppingList, ShoppingListItem, PantryLocation, Receipt } from '../../types';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
const mockQuery = vi.fn();
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Shopping DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getShoppingLists', () => {
|
||||
it('should execute the correct query and return shopping lists', async () => {
|
||||
const mockLists: ShoppingList[] = [{ shopping_list_id: 1, name: 'My List', user_id: 'user-123', created_at: '', items: [] }];
|
||||
mockQuery.mockResolvedValue({ rows: mockLists });
|
||||
|
||||
const result = await getShoppingLists('user-123');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.shopping_lists sl'), ['user-123']);
|
||||
expect(result).toEqual(mockLists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createShoppingList', () => {
|
||||
it('should execute an INSERT query and return the new list', async () => {
|
||||
const mockList: ShoppingList = { shopping_list_id: 1, name: 'New List', user_id: 'user-123', created_at: '', items: [] };
|
||||
mockQuery.mockResolvedValue({ rows: [mockList] });
|
||||
|
||||
const result = await createShoppingList('user-123', 'New List');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.shopping_lists'), ['user-123', 'New List']);
|
||||
expect(result).toEqual({ ...mockList, items: [] }); // Ensure items array is empty
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteShoppingList', () => {
|
||||
it('should execute a DELETE query with user ownership check', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await deleteShoppingList(1, 'user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('DELETE FROM public.shopping_lists WHERE shopping_list_id = $1 AND user_id = $2', [1, 'user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addShoppingListItem', () => {
|
||||
it('should execute an INSERT query for a custom item', async () => {
|
||||
const mockItem: ShoppingListItem = { shopping_list_item_id: 1, shopping_list_id: 1, custom_item_name: 'Custom Item', quantity: 1, is_purchased: false, added_at: '' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockItem] });
|
||||
|
||||
const result = await addShoppingListItem(1, { customItemName: 'Custom Item' });
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.shopping_list_items'), [1, undefined, 'Custom Item']);
|
||||
expect(result).toEqual(mockItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeShoppingListItem', () => {
|
||||
it('should execute a DELETE query', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await removeShoppingListItem(101);
|
||||
expect(getPool().query).toHaveBeenCalledWith('DELETE FROM public.shopping_list_items WHERE shopping_list_item_id = $1', [101]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateShoppingListForMenuPlan', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await generateShoppingListForMenuPlan(1, 'user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.generate_shopping_list_for_menu_plan($1, $2)', [1, 'user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMenuPlanToShoppingList', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await addMenuPlanToShoppingList(1, 2, 'user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.add_menu_plan_to_shopping_list($1, $2, $3)', [1, 2, 'user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPantryLocations', () => {
|
||||
it('should execute a SELECT query for pantry locations', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getPantryLocations('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.pantry_locations WHERE user_id = $1 ORDER BY name', ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPantryLocation', () => {
|
||||
it('should execute an INSERT query and return the new location', async () => {
|
||||
const mockLocation: PantryLocation = { pantry_location_id: 1, user_id: 'user-123', name: 'Fridge' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockLocation] });
|
||||
|
||||
const result = await createPantryLocation('user-123', 'Fridge');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.pantry_locations'), ['user-123', 'Fridge']);
|
||||
expect(result).toEqual(mockLocation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateShoppingListItem', () => {
|
||||
it('should build and execute an UPDATE query', async () => {
|
||||
const mockItem: ShoppingListItem = { shopping_list_item_id: 1, shopping_list_id: 1, custom_item_name: 'Item', quantity: 2, is_purchased: true, added_at: '' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockItem] });
|
||||
|
||||
const result = await updateShoppingListItem(1, { quantity: 2, is_purchased: true });
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
'UPDATE public.shopping_list_items SET quantity = $1, is_purchased = $2 WHERE shopping_list_item_id = $3 RETURNING *',
|
||||
[2, true, 1]
|
||||
);
|
||||
expect(result).toEqual(mockItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeShoppingList', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ complete_shopping_list: 10 }] });
|
||||
const result = await completeShoppingList(1, 'user-123', 5000);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT public.complete_shopping_list($1, $2, $3)', [1, 'user-123', 5000]);
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShoppingTripHistory', () => {
|
||||
it('should execute the correct query to get shopping trip history', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getShoppingTripHistory('user-123');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.shopping_trips st'), ['user-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReceipt', () => {
|
||||
it('should execute an INSERT query and return the new receipt', async () => {
|
||||
const mockReceipt: Receipt = { receipt_id: 1, user_id: 'user-123', receipt_image_url: '/img.jpg', status: 'pending', created_at: '' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockReceipt] });
|
||||
|
||||
const result = await createReceipt('user-123', '/img.jpg');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.receipts'), ['user-123', '/img.jpg']);
|
||||
expect(result).toEqual(mockReceipt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findDealsForReceipt', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await findDealsForReceipt(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.find_deals_for_receipt_items($1)', [1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findReceiptOwner', () => {
|
||||
it('should execute a SELECT query to find the owner of a receipt', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [{ user_id: 'user-123' }] });
|
||||
const result = await findReceiptOwner(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT user_id FROM public.receipts WHERE receipt_id = $1', [1]);
|
||||
expect(result?.user_id).toBe('user-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
102
src/utils/audioUtils.test.ts
Normal file
102
src/utils/audioUtils.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// src/utils/audioUtils.test.ts
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { encode, decode, decodeAudioData } from './audioUtils';
|
||||
|
||||
describe('audioUtils', () => {
|
||||
describe('encode and decode', () => {
|
||||
it('should correctly encode a Uint8Array to a base64 string', () => {
|
||||
// "Hello" in UTF-8
|
||||
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
const expectedBase64 = 'SGVsbG8=';
|
||||
expect(encode(bytes)).toBe(expectedBase64);
|
||||
});
|
||||
|
||||
it('should correctly decode a base64 string to a Uint8Array', () => {
|
||||
const base64String = 'SGVsbG8=';
|
||||
const expectedBytes = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
expect(decode(base64String)).toEqual(expectedBytes);
|
||||
});
|
||||
|
||||
it('should be inverse functions of each other', () => {
|
||||
const originalBytes = new Uint8Array([1, 2, 3, 255, 0, 128]);
|
||||
const encoded = encode(originalBytes);
|
||||
const decoded = decode(encoded);
|
||||
expect(decoded).toEqual(originalBytes);
|
||||
});
|
||||
|
||||
it('should handle empty arrays correctly', () => {
|
||||
const emptyBytes = new Uint8Array([]);
|
||||
expect(encode(emptyBytes)).toBe('');
|
||||
expect(decode('')).toEqual(emptyBytes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeAudioData', () => {
|
||||
// Mock the AudioContext and its methods for a Node.js environment
|
||||
const mockGetChannelData = vi.fn();
|
||||
const mockCreateBuffer = vi.fn(() => ({
|
||||
getChannelData: mockGetChannelData,
|
||||
}));
|
||||
|
||||
const mockAudioContext = {
|
||||
createBuffer: mockCreateBuffer,
|
||||
} as unknown as AudioContext;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call createBuffer with the correct parameters for mono audio', async () => {
|
||||
// 4 samples of 16-bit mono audio (8 bytes total)
|
||||
const pcmData = new Uint8Array([0, 1, 0, 2, 0, 3, 0, 4]);
|
||||
const sampleRate = 24000;
|
||||
const numChannels = 1;
|
||||
|
||||
await decodeAudioData(pcmData, mockAudioContext, sampleRate, numChannels);
|
||||
|
||||
// 4 samples (Int16) / 1 channel = 4 frames
|
||||
const expectedFrameCount = 4;
|
||||
expect(mockCreateBuffer).toHaveBeenCalledWith(numChannels, expectedFrameCount, sampleRate);
|
||||
});
|
||||
|
||||
it('should correctly normalize mono PCM data into the AudioBuffer', async () => {
|
||||
// 16-bit PCM data for values 256 and -512
|
||||
const pcmData = new Uint8Array([0, 1, 0, 254]); // Little-endian: 256, -512
|
||||
const channelData = new Float32Array(2);
|
||||
mockGetChannelData.mockReturnValue(channelData);
|
||||
|
||||
await decodeAudioData(pcmData, mockAudioContext, 24000, 1);
|
||||
|
||||
expect(mockGetChannelData).toHaveBeenCalledWith(0);
|
||||
// Check that the values were correctly normalized to the [-1.0, 1.0] range
|
||||
expect(channelData[0]).toBeCloseTo(256 / 32768.0);
|
||||
expect(channelData[1]).toBeCloseTo(-512 / 32768.0);
|
||||
});
|
||||
|
||||
it('should correctly de-interleave and normalize stereo PCM data', async () => {
|
||||
// 16-bit PCM data for two stereo frames: [L1, R1, L2, R2]
|
||||
// L1=256, R1=512, L2=-256, R2=-512
|
||||
const pcmData = new Uint8Array([0, 1, 0, 2, 0, 255, 0, 254]);
|
||||
const leftChannelData = new Float32Array(2);
|
||||
const rightChannelData = new Float32Array(2);
|
||||
|
||||
// Mock getChannelData to return the correct channel array based on the channel index
|
||||
mockGetChannelData.mockImplementation((channel: number) => {
|
||||
return channel === 0 ? leftChannelData : rightChannelData;
|
||||
});
|
||||
|
||||
await decodeAudioData(pcmData, mockAudioContext, 24000, 2);
|
||||
|
||||
expect(mockGetChannelData).toHaveBeenCalledWith(0); // Left channel
|
||||
expect(mockGetChannelData).toHaveBeenCalledWith(1); // Right channel
|
||||
|
||||
// Check left channel values
|
||||
expect(leftChannelData[0]).toBeCloseTo(256 / 32768.0);
|
||||
expect(leftChannelData[1]).toBeCloseTo(-256 / 32768.0);
|
||||
|
||||
// Check right channel values
|
||||
expect(rightChannelData[0]).toBeCloseTo(512 / 32768.0);
|
||||
expect(rightChannelData[1]).toBeCloseTo(-512 / 32768.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
45
src/utils/checksum.test.ts
Normal file
45
src/utils/checksum.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// src/utils/checksum.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { generateFileChecksum } from './checksum';
|
||||
|
||||
describe('generateFileChecksum', () => {
|
||||
it('should generate the correct SHA-256 checksum for a simple text file', async () => {
|
||||
// Arrange
|
||||
const fileContent = 'Hello, world!';
|
||||
const file = new File([fileContent], 'hello.txt', { type: 'text/plain' });
|
||||
// The known SHA-256 hash for "Hello, world!"
|
||||
const expectedChecksum = '315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3';
|
||||
|
||||
// Act
|
||||
const checksum = await generateFileChecksum(file);
|
||||
|
||||
// Assert
|
||||
expect(checksum).toBe(expectedChecksum);
|
||||
});
|
||||
|
||||
it('should generate the correct SHA-256 checksum for an empty file', async () => {
|
||||
// Arrange
|
||||
const file = new File([], 'empty.txt', { type: 'text/plain' });
|
||||
// The known SHA-256 hash for an empty input
|
||||
const expectedChecksum = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
||||
|
||||
// Act
|
||||
const checksum = await generateFileChecksum(file);
|
||||
|
||||
// Assert
|
||||
expect(checksum).toBe(expectedChecksum);
|
||||
});
|
||||
|
||||
it('should generate a consistent checksum for a file with binary data', async () => {
|
||||
// Arrange: Create a file with some arbitrary binary data
|
||||
const binaryData = new Uint8Array([0, 1, 2, 3, 255, 254, 128]);
|
||||
const file = new File([binaryData], 'data.bin', { type: 'application/octet-stream' });
|
||||
const expectedChecksum = '8d3744339743449b433e2260b9e8334c25334edea12a8363722319385303159b';
|
||||
|
||||
// Act
|
||||
const checksum = await generateFileChecksum(file);
|
||||
|
||||
// Assert
|
||||
expect(checksum).toBe(expectedChecksum);
|
||||
});
|
||||
});
|
||||
57
src/utils/objectUtils.test.ts
Normal file
57
src/utils/objectUtils.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// src/utils/objectUtils.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { omit } from './objectUtils';
|
||||
|
||||
describe('omit', () => {
|
||||
const sourceObject = {
|
||||
a: 1,
|
||||
b: 'hello',
|
||||
c: true,
|
||||
d: { nested: 'world' },
|
||||
};
|
||||
|
||||
it('should omit a single key from an object', () => {
|
||||
const result = omit(sourceObject, ['a']);
|
||||
expect(result).toEqual({ b: 'hello', c: true, d: { nested: 'world' } });
|
||||
expect(result).not.toHaveProperty('a');
|
||||
});
|
||||
|
||||
it('should omit multiple keys from an object', () => {
|
||||
const result = omit(sourceObject, ['a', 'c']);
|
||||
expect(result).toEqual({ b: 'hello', d: { nested: 'world' } });
|
||||
expect(result).not.toHaveProperty('a');
|
||||
expect(result).not.toHaveProperty('c');
|
||||
});
|
||||
|
||||
it('should return a new object without mutating the original', () => {
|
||||
const original = { ...sourceObject };
|
||||
omit(original, ['b']);
|
||||
// Check that the original object is unchanged
|
||||
expect(original).toEqual(sourceObject);
|
||||
});
|
||||
|
||||
it('should return an identical object if no keys are omitted', () => {
|
||||
const result = omit(sourceObject, []);
|
||||
expect(result).toEqual(sourceObject);
|
||||
// It should be a shallow copy, not the same object reference
|
||||
expect(result).not.toBe(sourceObject);
|
||||
});
|
||||
|
||||
it('should handle omitting keys that do not exist on the object', () => {
|
||||
// The type system should ideally prevent this, but we test the runtime behavior.
|
||||
const result = omit(sourceObject, ['e' as 'a']); // Cast to satisfy TypeScript
|
||||
expect(result).toEqual(sourceObject);
|
||||
});
|
||||
|
||||
it('should handle an empty source object', () => {
|
||||
// @ts-expect-error - We are intentionally testing the runtime behavior of
|
||||
// omitting a key from an object that doesn't have it.
|
||||
const result = omit({}, ['a']);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle omitting all keys from an object', () => {
|
||||
const result = omit(sourceObject, ['a', 'b', 'c', 'd']);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
111
src/utils/pdfConverter.test.ts
Normal file
111
src/utils/pdfConverter.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
// src/utils/pdfConverter.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { convertPdfToImageFiles } from './pdfConverter';
|
||||
|
||||
// Mock the entire pdfjs-dist library
|
||||
const mockPdfPage = {
|
||||
getViewport: vi.fn(() => ({ width: 100, height: 150 })),
|
||||
render: vi.fn(() => ({ promise: Promise.resolve() })),
|
||||
};
|
||||
|
||||
const mockPdfDocument = {
|
||||
numPages: 3,
|
||||
getPage: vi.fn(() => Promise.resolve(mockPdfPage)),
|
||||
};
|
||||
|
||||
vi.mock('pdfjs-dist', () => ({
|
||||
getDocument: vi.fn(() => ({ promise: Promise.resolve(mockPdfDocument) })),
|
||||
}));
|
||||
|
||||
// Mock the browser's Canvas API
|
||||
const mockToBlob = vi.fn((callback) => {
|
||||
const blob = new Blob(['mock-jpeg-content'], { type: 'image/jpeg' });
|
||||
callback(blob);
|
||||
});
|
||||
|
||||
const mockGetContext = vi.fn(() => ({}));
|
||||
|
||||
// We need to mock document.createElement to return our mock canvas
|
||||
const originalCreateElement = document.createElement;
|
||||
Object.defineProperty(document, 'createElement', {
|
||||
writable: true,
|
||||
value: vi.fn((tag: string) => {
|
||||
if (tag === 'canvas') {
|
||||
return {
|
||||
getContext: mockGetContext,
|
||||
toBlob: mockToBlob,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
return originalCreateElement.call(document, tag);
|
||||
}),
|
||||
});
|
||||
|
||||
// Mock the global File constructor
|
||||
vi.stubGlobal('File', vi.fn((blobParts, fileName, options) => ({
|
||||
blobParts,
|
||||
name: fileName,
|
||||
options,
|
||||
})));
|
||||
|
||||
describe('pdfConverter', () => {
|
||||
beforeEach(() => {
|
||||
// Clear all mock history before each test
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should convert a multi-page PDF to image files', async () => {
|
||||
const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
|
||||
const onProgress = vi.fn();
|
||||
|
||||
const { imageFiles, pageCount } = await convertPdfToImageFiles(pdfFile, onProgress);
|
||||
|
||||
// Verify that getDocument was called
|
||||
const { getDocument } = await import('pdfjs-dist');
|
||||
expect(getDocument).toHaveBeenCalled();
|
||||
|
||||
// Verify page count and file count
|
||||
expect(pageCount).toBe(3);
|
||||
expect(imageFiles).toHaveLength(3);
|
||||
|
||||
// Verify that getPage was called for each page
|
||||
expect(mockPdfDocument.getPage).toHaveBeenCalledTimes(3);
|
||||
expect(mockPdfDocument.getPage).toHaveBeenCalledWith(1);
|
||||
expect(mockPdfDocument.getPage).toHaveBeenCalledWith(2);
|
||||
expect(mockPdfDocument.getPage).toHaveBeenCalledWith(3);
|
||||
|
||||
// Verify that render was called for each page
|
||||
expect(mockPdfPage.render).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Verify that the File constructor was called with the correct names
|
||||
expect(File).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_1.jpeg', { type: 'image/jpeg' });
|
||||
expect(File).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_2.jpeg', { type: 'image/jpeg' });
|
||||
expect(File).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_3.jpeg', { type: 'image/jpeg' });
|
||||
});
|
||||
|
||||
it('should call the onProgress callback for each page', async () => {
|
||||
const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await convertPdfToImageFiles(pdfFile, onProgress);
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(3);
|
||||
expect(onProgress).toHaveBeenCalledWith(1, 3);
|
||||
expect(onProgress).toHaveBeenCalledWith(2, 3);
|
||||
expect(onProgress).toHaveBeenCalledWith(3, 3);
|
||||
});
|
||||
|
||||
it('should throw an error if conversion results in zero images for a non-empty PDF', async () => {
|
||||
const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
|
||||
|
||||
// Mock toBlob to fail by returning null
|
||||
mockToBlob.mockImplementationOnce((callback) => callback(null))
|
||||
.mockImplementationOnce((callback) => callback(null))
|
||||
.mockImplementationOnce((callback) => callback(null));
|
||||
|
||||
await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow(
|
||||
'Failed to convert page 1 of PDF to blob.'
|
||||
);
|
||||
});
|
||||
});
|
||||
73
src/utils/priceParser.test.ts
Normal file
73
src/utils/priceParser.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// src/utils/priceParser.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parsePriceToCents } from './priceParser';
|
||||
|
||||
describe('parsePriceToCents', () => {
|
||||
// Test cases for standard dollar formats
|
||||
it('should parse standard dollar formats correctly', () => {
|
||||
expect(parsePriceToCents('$10.99')).toBe(1099);
|
||||
expect(parsePriceToCents('10.99')).toBe(1099);
|
||||
expect(parsePriceToCents('$0.99')).toBe(99);
|
||||
expect(parsePriceToCents('.99')).toBe(99);
|
||||
expect(parsePriceToCents('$5')).toBe(500);
|
||||
expect(parsePriceToCents('5')).toBe(500);
|
||||
});
|
||||
|
||||
// Test cases for cents format
|
||||
it('should parse cents format correctly', () => {
|
||||
expect(parsePriceToCents('99¢')).toBe(99);
|
||||
expect(parsePriceToCents('99 ¢')).toBe(99);
|
||||
expect(parsePriceToCents('125¢')).toBe(125);
|
||||
});
|
||||
|
||||
// Test cases for rounding
|
||||
it('should round decimal cents to the nearest whole number', () => {
|
||||
expect(parsePriceToCents('89.9¢')).toBe(90);
|
||||
expect(parsePriceToCents('89.1¢')).toBe(89);
|
||||
});
|
||||
|
||||
it('should round decimal dollars to the nearest cent', () => {
|
||||
expect(parsePriceToCents('$10.995')).toBe(1100);
|
||||
expect(parsePriceToCents('1.234')).toBe(123);
|
||||
});
|
||||
|
||||
// Test cases for the special "already in cents" logic
|
||||
it('should treat whole numbers over 50 as cents', () => {
|
||||
expect(parsePriceToCents('399')).toBe(399); // e.g., for $3.99
|
||||
expect(parsePriceToCents('1299')).toBe(1299); // e.g., for $12.99
|
||||
expect(parsePriceToCents('51')).toBe(51);
|
||||
});
|
||||
|
||||
it('should treat whole numbers 50 or less as dollars', () => {
|
||||
expect(parsePriceToCents('49')).toBe(4900);
|
||||
expect(parsePriceToCents('50')).toBe(5000);
|
||||
expect(parsePriceToCents('1')).toBe(100);
|
||||
});
|
||||
|
||||
// Test cases for invalid or unhandled formats
|
||||
it('should return null for "X for Y" formats', () => {
|
||||
expect(parsePriceToCents('2 for $5.00')).toBeNull();
|
||||
expect(parsePriceToCents('3 for 10')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-price strings', () => {
|
||||
expect(parsePriceToCents('FREE')).toBeNull();
|
||||
expect(parsePriceToCents('See in store')).toBeNull();
|
||||
expect(parsePriceToCents('abc')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty or invalid inputs', () => {
|
||||
expect(parsePriceToCents('')).toBeNull();
|
||||
expect(parsePriceToCents(' ')).toBeNull();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(parsePriceToCents(null as any)).toBeNull();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(parsePriceToCents(undefined as any)).toBeNull();
|
||||
});
|
||||
|
||||
// Test cases for whitespace handling
|
||||
it('should handle leading/trailing whitespace', () => {
|
||||
expect(parsePriceToCents(' $10.99 ')).toBe(1099);
|
||||
expect(parsePriceToCents(' 99¢ ')).toBe(99);
|
||||
});
|
||||
});
|
||||
90
src/utils/processingTimer.test.ts
Normal file
90
src/utils/processingTimer.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// src/utils/processingTimer.test.ts
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { recordProcessingTime, getAverageProcessingTime } from './processingTimer';
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../services/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const PROCESSING_TIMES_KEY = 'flyerProcessingTimes';
|
||||
|
||||
// Create an in-memory mock for localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe('processingTimer', () => {
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test to ensure isolation
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('recordProcessingTime', () => {
|
||||
it('should add a new time to an empty list', () => {
|
||||
recordProcessingTime(50);
|
||||
const stored = localStorage.getItem(PROCESSING_TIMES_KEY);
|
||||
expect(JSON.parse(stored!)).toEqual([50]);
|
||||
});
|
||||
|
||||
it('should add a new time to an existing list', () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, JSON.stringify([40, 45]));
|
||||
recordProcessingTime(50);
|
||||
const stored = localStorage.getItem(PROCESSING_TIMES_KEY);
|
||||
expect(JSON.parse(stored!)).toEqual([40, 45, 50]);
|
||||
});
|
||||
|
||||
it('should keep only the last 5 samples', () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, JSON.stringify([10, 20, 30, 40, 50]));
|
||||
recordProcessingTime(60);
|
||||
const stored = localStorage.getItem(PROCESSING_TIMES_KEY);
|
||||
expect(JSON.parse(stored!)).toEqual([20, 30, 40, 50, 60]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAverageProcessingTime', () => {
|
||||
it('should return the default value of 45 if no data exists', () => {
|
||||
expect(getAverageProcessingTime()).toBe(45);
|
||||
});
|
||||
|
||||
it('should return the default value if the stored data is an empty array', () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, JSON.stringify([]));
|
||||
expect(getAverageProcessingTime()).toBe(45);
|
||||
});
|
||||
|
||||
it('should correctly calculate the average of stored times', () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, JSON.stringify([30, 40, 50]));
|
||||
// (30 + 40 + 50) / 3 = 40
|
||||
expect(getAverageProcessingTime()).toBe(40);
|
||||
});
|
||||
|
||||
it('should round the average to the nearest integer', () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, JSON.stringify([31, 41, 51]));
|
||||
// (31 + 41 + 51) / 3 = 41
|
||||
expect(getAverageProcessingTime()).toBe(41);
|
||||
});
|
||||
|
||||
it('should return the default value if localStorage contains invalid JSON', async () => {
|
||||
localStorage.setItem(PROCESSING_TIMES_KEY, 'not-json');
|
||||
expect(getAverageProcessingTime()).toBe(45);
|
||||
// Also check that the error was logged
|
||||
const { logger } = await import('../services/logger');
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
50
src/utils/timeout.test.ts
Normal file
50
src/utils/timeout.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// src/utils/timeout.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { withTimeout } from './timeout';
|
||||
|
||||
describe('withTimeout', () => {
|
||||
beforeEach(() => {
|
||||
// Use fake timers to control setTimeout and clearTimeout
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore real timers after each test
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should resolve with the original promise value if it resolves before the timeout', async () => {
|
||||
const slowPromise = new Promise<string>(resolve => setTimeout(() => resolve('Success'), 100));
|
||||
const wrappedPromise = withTimeout(slowPromise, 500);
|
||||
|
||||
// Advance timers just enough for the promise to resolve
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// The promise should resolve successfully
|
||||
await expect(wrappedPromise).resolves.toBe('Success');
|
||||
});
|
||||
|
||||
it('should reject with the original promise reason if it rejects before the timeout', async () => {
|
||||
const failingPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Original error')), 100)
|
||||
);
|
||||
const wrappedPromise = withTimeout(failingPromise, 500);
|
||||
|
||||
// Advance timers just enough for the promise to reject
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// The promise should reject with the original error
|
||||
await expect(wrappedPromise).rejects.toThrow('Original error');
|
||||
});
|
||||
|
||||
it('should reject with a timeout error if the promise does not resolve in time', async () => {
|
||||
const verySlowPromise = new Promise<string>(resolve => setTimeout(() => resolve('Success'), 1000));
|
||||
const wrappedPromise = withTimeout(verySlowPromise, 500);
|
||||
|
||||
// Advance timers past the timeout duration but before the promise resolves
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
// The promise should reject with the specific timeout error
|
||||
await expect(wrappedPromise).rejects.toThrow('Operation timed out after 0.5 seconds');
|
||||
});
|
||||
});
|
||||
117
src/utils/unitConverter.test.ts
Normal file
117
src/utils/unitConverter.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// src/utils/unitConverter.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatUnitPrice, convertToMetric } from './unitConverter';
|
||||
import type { UnitPrice } from '../types';
|
||||
|
||||
describe('formatUnitPrice', () => {
|
||||
it('should return a placeholder for null or invalid input', () => {
|
||||
expect(formatUnitPrice(null, 'metric')).toEqual({ price: '—', unit: null });
|
||||
expect(formatUnitPrice(undefined, 'imperial')).toEqual({ price: '—', unit: null });
|
||||
const invalidInput = { value: 'not-a-number' } as unknown as UnitPrice;
|
||||
expect(formatUnitPrice(invalidInput, 'metric')).toEqual({ price: '—', unit: null });
|
||||
});
|
||||
|
||||
// --- No Conversion Tests ---
|
||||
it('should format a metric price correctly when the system is metric', () => {
|
||||
const unitPrice: UnitPrice = { value: 220, unit: 'kg' }; // $2.20/kg
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$2.20', unit: '/kg' });
|
||||
});
|
||||
|
||||
it('should format an imperial price correctly when the system is imperial', () => {
|
||||
const unitPrice: UnitPrice = { value: 100, unit: 'lb' }; // $1.00/lb
|
||||
expect(formatUnitPrice(unitPrice, 'imperial')).toEqual({ price: '$1.00', unit: '/lb' });
|
||||
});
|
||||
|
||||
it('should format an "each" price correctly regardless of system', () => {
|
||||
const unitPrice: UnitPrice = { value: 500, unit: 'each' }; // $5.00/each
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$5.00', unit: '/each' });
|
||||
expect(formatUnitPrice(unitPrice, 'imperial')).toEqual({ price: '$5.00', unit: '/each' });
|
||||
});
|
||||
|
||||
// --- Conversion Tests ---
|
||||
it('should convert a metric price (kg) to imperial (lb)', () => {
|
||||
const unitPrice: UnitPrice = { value: 220, unit: 'kg' }; // $2.20/kg
|
||||
// 2.20 / 2.20462 = 0.9979... -> $1.00/lb
|
||||
expect(formatUnitPrice(unitPrice, 'imperial')).toEqual({ price: '$1.00', unit: '/lb' });
|
||||
});
|
||||
|
||||
it('should convert an imperial price (lb) to metric (kg)', () => {
|
||||
const unitPrice: UnitPrice = { value: 100, unit: 'lb' }; // $1.00/lb
|
||||
// 1.00 / 0.453592 = 2.2046... -> $2.20/kg
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$2.20', unit: '/kg' });
|
||||
});
|
||||
|
||||
it('should convert a metric price (g) to imperial (oz)', () => {
|
||||
const unitPrice: UnitPrice = { value: 10, unit: 'g' }; // $0.10/g
|
||||
// 0.10 / 0.035274 = 2.83... -> $2.83/oz
|
||||
expect(formatUnitPrice(unitPrice, 'imperial')).toEqual({ price: '$2.83', unit: '/oz' });
|
||||
});
|
||||
|
||||
it('should convert an imperial price (oz) to metric (g)', () => {
|
||||
const unitPrice: UnitPrice = { value: 50, unit: 'oz' }; // $0.50/oz
|
||||
// 0.50 / 28.3495 = 0.0176... -> $0.018/g
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$0.018', unit: '/g' });
|
||||
});
|
||||
|
||||
it('should convert a metric price (l) to imperial (fl oz)', () => {
|
||||
const unitPrice: UnitPrice = { value: 100, unit: 'l' }; // $1.00/l
|
||||
// 1.00 / 33.814 = 0.0295... -> $0.030/fl oz
|
||||
expect(formatUnitPrice(unitPrice, 'imperial')).toEqual({ price: '$0.030', unit: '/fl oz' });
|
||||
});
|
||||
|
||||
// --- Formatting Tests ---
|
||||
it('should use 3 decimal places for prices less than 10 cents', () => {
|
||||
const unitPrice: UnitPrice = { value: 5, unit: 'g' }; // $0.05/g
|
||||
// 0.05 / 28.3495 = 0.00176... -> $0.002/g
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$0.050', unit: '/g' });
|
||||
});
|
||||
|
||||
it('should use 2 decimal places for prices 10 cents or more', () => {
|
||||
const unitPrice: UnitPrice = { value: 15, unit: 'g' }; // $0.15/g
|
||||
expect(formatUnitPrice(unitPrice, 'metric')).toEqual({ price: '$0.15', unit: '/g' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToMetric', () => {
|
||||
it('should return null or undefined if input is null or undefined', () => {
|
||||
expect(convertToMetric(null)).toBeNull();
|
||||
expect(convertToMetric(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not convert an already metric unit', () => {
|
||||
const metricPrice: UnitPrice = { value: 199, unit: 'kg' };
|
||||
expect(convertToMetric(metricPrice)).toEqual(metricPrice);
|
||||
});
|
||||
|
||||
it('should not convert a non-standard unit like "each"', () => {
|
||||
const eachPrice: UnitPrice = { value: 500, unit: 'each' };
|
||||
expect(convertToMetric(eachPrice)).toEqual(eachPrice);
|
||||
});
|
||||
|
||||
it('should convert from lb to kg', () => {
|
||||
const imperialPrice: UnitPrice = { value: 100, unit: 'lb' }; // $1.00/lb
|
||||
const expectedValue = 100 * 0.453592; // 45.3592
|
||||
expect(convertToMetric(imperialPrice)).toEqual({
|
||||
value: expectedValue,
|
||||
unit: 'kg',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert from oz to g', () => {
|
||||
const imperialPrice: UnitPrice = { value: 10, unit: 'oz' }; // $0.10/oz
|
||||
const expectedValue = 10 * 28.3495; // 283.495
|
||||
expect(convertToMetric(imperialPrice)).toEqual({
|
||||
value: expectedValue,
|
||||
unit: 'g',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert from fl oz to ml', () => {
|
||||
const imperialPrice: UnitPrice = { value: 5, unit: 'fl oz' }; // $0.05/fl oz
|
||||
const expectedValue = 5 * 29.5735; // 147.8675
|
||||
expect(convertToMetric(imperialPrice)).toEqual({
|
||||
value: expectedValue,
|
||||
unit: 'ml',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user