come on ai get it right
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m52s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m52s
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user