come on ai get it right
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m52s

This commit is contained in:
2025-12-01 21:08:04 -08:00
parent e445a08771
commit 3ebb025ed0
2 changed files with 57 additions and 51 deletions

View File

@@ -1,13 +1,12 @@
// src/services/aiApiClient.test.ts
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { http, HttpResponse, passthrough } from 'msw';
// Ensure the module under test is NOT mocked.
vi.unmock('./aiApiClient');
import * as aiApiClient from './aiApiClient';
import type { MasterGroceryItem } from '../types';
// 1. Mock logger to keep output clean
vi.mock('./logger', () => ({
@@ -21,7 +20,10 @@ vi.mock('./logger', () => ({
// 2. Mock ./apiClient to simply pass calls through to the global fetch.
vi.mock('./apiClient', () => ({
apiFetchWithAuth: (url: string, options: RequestInit) => {
const fullUrl = url.startsWith('/') ? `http://localhost${url}` : url;
// The base URL must match what MSW is expecting.
const fullUrl = url.startsWith('/')
? `http://localhost/api${url}`
: url;
return fetch(fullUrl, options);
},
}));
@@ -30,7 +32,7 @@ vi.mock('./apiClient', () => ({
const requestSpy = vi.fn();
const server = setupServer(
http.post('http://localhost/ai/:endpoint', async ({ request, params }) => {
http.post('http://localhost/api/ai/:endpoint', async ({ request, params }) => {
let body: any = {};
const contentType = request.headers.get('content-type');
@@ -40,8 +42,14 @@ const server = setupServer(
} catch (e) { /* ignore parse error */ }
} else if (contentType?.includes('multipart/form-data')) {
try {
// This is the key part. We read the formData from the request.
const formData = await request.formData();
body = Object.fromEntries(formData.entries());
// And then convert it to a plain object for easier assertions.
// This correctly preserves File objects.
body = {};
for (const [key, value] of formData.entries()) {
body[key] = value;
}
body._isFormData = true;
} catch (e) { /* ignore parse error */ }
}
@@ -69,11 +77,12 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('isImageAFlyer', () => {
it('should construct FormData and send a POST request', async () => {
// FIX: Use a plain object that mimics the File interface.
// MSW's `request.formData()` in this test environment loses the original filename,
// so we create a file-like object that will pass the assertion.
const mockFile = { name: 'flyer.jpg', size: 123, type: 'image/jpeg' };
await aiApiClient.isImageAFlyer(mockFile as any, 'test-token');
// FIX: Create a Blob and append it to FormData with a filename.
// This is the most reliable way to simulate a file upload in this test environment.
const blob = new Blob(['dummy'], { type: 'image/jpeg' });
const formData = new FormData();
formData.append('image', blob, 'flyer.jpg');
await aiApiClient.isImageAFlyer(formData as any, 'test-token');
expect(requestSpy).toHaveBeenCalledTimes(1);
const req = requestSpy.mock.calls[0][0];
@@ -89,8 +98,10 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('extractAddressFromImage', () => {
it('should construct FormData and send a POST request', async () => {
const mockFile = { name: 'flyer.jpg', size: 123, type: 'image/jpeg' };
await aiApiClient.extractAddressFromImage(mockFile as any, 'test-token');
const blob = new Blob(['dummy'], { type: 'image/jpeg' });
const formData = new FormData();
formData.append('image', blob, 'flyer.jpg');
await aiApiClient.extractAddressFromImage(formData as any, 'test-token');
expect(requestSpy).toHaveBeenCalledTimes(1);
const req = requestSpy.mock.calls[0][0];
@@ -103,10 +114,14 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('extractCoreDataFromImage', () => {
it('should construct FormData with files and master items JSON', async () => {
const files = [{ name: 'f1.jpg' }, { name: 'f2.jpg' }];
const masterItems: MasterGroceryItem[] = [{ master_grocery_item_id: 1, name: 'Milk', created_at: '' }];
const blob1 = new Blob(['f1'], { type: 'image/jpeg' });
const blob2 = new Blob(['f2'], { type: 'image/jpeg' });
const formData = new FormData();
formData.append('flyerImages', blob1, 'f1.jpg');
formData.append('flyerImages', blob2, 'f2.jpg');
const masterItems = [{ master_grocery_item_id: 1, name: 'Milk' }];
await aiApiClient.extractCoreDataFromImage(files as any, masterItems);
await aiApiClient.extractCoreDataFromImage(formData as any, masterItems as any);
expect(requestSpy).toHaveBeenCalledTimes(1);
const req = requestSpy.mock.calls[0][0];
@@ -114,27 +129,18 @@ describe('AI API Client (Network Mocking with MSW)', () => {
expect(req.endpoint).toBe('process-flyer');
expect(req.body._isFormData).toBe(true);
// Check that a file field exists (FormData conflation in object conversion keeps last key usually)
expect(req.body.flyerImages).toHaveProperty('name');
// Note: When multiple files are appended with the same key, `formData.get()` returns the first one.
expect(req.body.flyerImages).toHaveProperty('name', 'f1.jpg');
expect(req.body.masterItems).toBe(JSON.stringify(masterItems));
});
});[{
"resource": "/d:/gitea/flyer-crawler.projectium.com/flyer-crawler.projectium.com/src/services/aiApiClient.test.ts",
"owner": "typescript",
"code": "2345",
"severity": 8,
"message": "Argument of type '{ name: string; }[]' is not assignable to parameter of type 'File[]'.\n Type '{ name: string; }' is missing the following properties from type 'File': lastModified, webkitRelativePath, size, type, and 5 more.",
"source": "ts",
"startLineNumber": 109,
"startColumn": 50,
"endLineNumber": 109,
"endColumn": 55,
"origin": "extHost1"
}]
});
describe('extractLogoFromImage', () => {
it('should construct FormData and send a POST request', async () => {
const files = [{ name: 'logo.jpg' }];
await aiApiClient.extractLogoFromImage(files as any, 'test-token');
const blob = new Blob(['logo'], { type: 'image/jpeg' });
const formData = new FormData();
formData.append('images', blob, 'logo.jpg');
await aiApiClient.extractLogoFromImage(formData as any, 'test-token');
expect(requestSpy).toHaveBeenCalledTimes(1);
const req = requestSpy.mock.calls[0][0];

View File

@@ -1,16 +1,11 @@
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
import { notifySuccess, notifyError } from './notificationService';
// We do NOT need to mock the module anymore because we are injecting the dependency.
// vi.mock('../lib/toast'); <--- Removed
// FIX: Mock the local re-export, not the library directly.
// This is more stable and ensures the service under test gets the mock.
vi.mock('../lib/toast');
describe('Notification Service', () => {
// Strategy #2: Create a simple mock object that matches the interface
const mockToaster = {
success: vi.fn(),
error: vi.fn(),
};
beforeAll(() => {
// Strategy #6: Verify JSDOM Window and stub missing browser APIs.
// libraries like react-hot-toast often rely on window.matchMedia or similar.
@@ -35,18 +30,22 @@ describe('Notification Service', () => {
});
beforeEach(() => {
// We need to reset modules to re-import the service with the mock
vi.resetModules();
vi.clearAllMocks();
});
describe('notifySuccess', () => {
it('should call the injected toaster.success with correct options', () => {
it('should call the injected toaster.success with correct options', async () => {
// Dynamically import the modules AFTER mocks are set up
const { default: toast } = await import('../lib/toast');
const { notifySuccess } = await import('./notificationService');
const message = 'Operation was successful!';
// Inject the mock explicitly
notifySuccess(message, mockToaster);
expect(mockToaster.success).toHaveBeenCalledTimes(1);
expect(mockToaster.success).toHaveBeenCalledWith(
notifySuccess(message);
expect(toast.success).toHaveBeenCalledTimes(1);
expect(toast.success).toHaveBeenCalledWith(
message,
expect.objectContaining({
style: expect.any(Object),
@@ -60,14 +59,15 @@ describe('Notification Service', () => {
});
describe('notifyError', () => {
it('should call the injected toaster.error with correct options', () => {
it('should call the injected toaster.error with correct options', async () => {
const { default: toast } = await import('../lib/toast');
const { notifyError } = await import('./notificationService');
const message = 'Something went wrong!';
// Inject the mock explicitly
notifyError(message, mockToaster);
expect(mockToaster.error).toHaveBeenCalledTimes(1);
expect(mockToaster.error).toHaveBeenCalledWith(
notifyError(message);
expect(toast.error).toHaveBeenCalledTimes(1);
expect(toast.error).toHaveBeenCalledWith(
message,
expect.objectContaining({
style: expect.any(Object),