one moar time - we can do it?
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 34m45s

This commit is contained in:
2025-12-17 10:49:46 -08:00
parent d3ad50cde6
commit ed2eb1743e
8 changed files with 179 additions and 140 deletions

View File

@@ -21,26 +21,34 @@ vi.mock('./logger.client', () => ({
vi.mock('./apiClient', async (importOriginal) => {
const actual = await importOriginal<typeof import('./apiClient')>();
return {
apiFetch: (url: string, options: RequestInit = {}, apiOptions: import('./apiClient').ApiOptions = {}) => {
// The base URL must match what MSW is expecting.
const fullUrl = url.startsWith('/')
? `http://localhost/api${url}`
: url;
// FIX: Correctly handle the tokenOverride by merging it into the request headers.
if (apiOptions.tokenOverride) {
options.headers = { ...options.headers, Authorization: `Bearer ${apiOptions.tokenOverride}` };
}
apiFetch: (url: string, options: RequestInit = {}, apiOptions: import('./apiClient').ApiOptions = {}) => {
const fullUrl = url.startsWith('/') ? `http://localhost/api${url}` : url;
options.headers = new Headers(options.headers); // Ensure headers is a Headers object
// FIX: Manually construct a Request object. This ensures that when `options.body`
// is FormData, the contained File objects are correctly processed by MSW's parsers,
// preserving their original filenames instead of defaulting to "blob".
return fetch(new Request(fullUrl, options));
// FIX: Manually construct a Request object. This is a critical step. When `fetch` is
// called directly with FormData, some environments (like JSDOM) can lose the filename.
// Wrapping it in `new Request()` helps preserve the metadata.
const request = new Request(fullUrl, options);
console.log(`[apiFetch MOCK] Created Request object for URL: ${request.url}. Content-Type will be set by browser/fetch.`);
return fetch(request);
if (apiOptions.tokenOverride) {
options.headers.set('Authorization', `Bearer ${apiOptions.tokenOverride}`);
}
// ================================= WORKAROUND FOR JSDOM FILE NAME BUG =================================
// JSDOM's fetch implementation (undici) loses filenames in FormData.
// SOLUTION: Before fetch is called, we find the file, extract its real name,
// and add it to a custom header. The MSW handler will read this header.
if (options.body instanceof FormData) {
console.log(`[apiFetch MOCK] FormData detected. Searching for file to preserve its name.`);
for (const value of (options.body as FormData).values()) {
if (value instanceof File) {
console.log(`[apiFetch MOCK] Found file: '${value.name}'. Setting 'X-Test-Filename' header.`);
options.headers.set('X-Test-Filename', value.name);
// We only expect one file per request in these tests, so we can break.
break;
}
}
}
// ======================================= END WORKAROUND ===============================================
const request = new Request(fullUrl, options);
console.log(`[apiFetch MOCK] Executing fetch for URL: ${request.url}.`);
return fetch(request);
},
// Add a mock for ApiOptions to satisfy the compiler
ApiOptions: vi.fn()
@@ -55,32 +63,30 @@ const server = setupServer(
let body: Record<string, unknown> | FormData = {};
let bodyForSpy: Record<string, unknown> = {};
const contentType = request.headers.get('Content-Type');
console.log(`\n--- [MSW HANDLER] Intercepted POST to '${String(params.endpoint)}'. Content-Type: ${contentType} ---`);
console.log(`\n--- [MSW HANDLER] Intercepted POST to '${String(params.endpoint)}' ---`);
if (contentType?.includes('application/json')) {
const parsedBody = await request.json();
console.log('[MSW HANDLER] Body is JSON. Parsed:', parsedBody);
if (typeof parsedBody === 'object' && parsedBody !== null && !Array.isArray(parsedBody)) {
body = parsedBody as Record<string, unknown>;
bodyForSpy = body; // For JSON, the body is already a plain object.
}
} else if (contentType?.includes('multipart/form-data')) {
body = await request.formData();
console.log('[MSW HANDLER] Body is FormData. Iterating entries...');
// FIX: The `instanceof File` check is unreliable in JSDOM.
// We will use "duck typing" to check if an object looks like a file.
// WORKAROUND PART 2: Read the filename from our custom header.
const preservedFilename = request.headers.get('X-Test-Filename');
console.log(`[MSW HANDLER] Reading 'X-Test-Filename' header. Value: '${preservedFilename}'`);
for (const [key, value] of (body as FormData).entries()) {
// A robust check for a File-like object.
const isFile = typeof value === 'object' && value !== null && 'name' in value && 'size' in value && 'type' in value;
console.log(`[MSW HANDLER] FormData Entry -> Key: '${key}', Type: ${typeof value}, IsFile: ${isFile}`);
if (isFile) {
const file = value as File;
console.log(`[MSW HANDLER DEBUG] -> Identified as File. Name: '${file.name}', Size: ${file.size}, Type: '${file.type}'`);
const finalName = preservedFilename || file.name;
console.log(`[MSW HANDLER DEBUG] Found file-like object for key '${key}'. Original name: '${file.name}'. Using preserved name: '${finalName}'`);
if (!bodyForSpy[key]) {
bodyForSpy[key] = { name: file.name, size: file.size, type: file.type };
bodyForSpy[key] = { name: finalName, size: file.size, type: file.type };
}
} else {
console.log(`[MSW HANDLER DEBUG] Found text field. Key: '${key}', Value: '${String(value)}'`);
bodyForSpy[key] = value;
}
}
@@ -121,7 +127,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('uploadAndProcessFlyer', () => {
it('should construct FormData with file and checksum and send a POST request', async () => {
const mockFile = new File(['dummy-flyer-content'], 'flyer.pdf', { type: 'application/pdf' });
const mockFile = new File(['this is a test pdf'], 'flyer.pdf', { type: 'application/pdf' });
const checksum = 'checksum-abc-123';
console.log(`\n--- [TEST START] uploadAndProcessFlyer ---`);
console.log('[TEST ARRANGE] Created mock file:', { name: mockFile.name, size: mockFile.size, type: mockFile.type });
@@ -168,7 +174,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('isImageAFlyer', () => {
it('should construct FormData and send a POST request', async () => {
const mockFile = new File(['dummy'], 'flyer.jpg', { type: 'image/jpeg' });
const mockFile = new File(['dummy image content'], 'flyer.jpg', { type: 'image/jpeg' });
console.log(`\n--- [TEST START] isImageAFlyer ---`);
await aiApiClient.isImageAFlyer(mockFile, 'test-token');
@@ -189,7 +195,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('extractAddressFromImage', () => {
it('should construct FormData and send a POST request', async () => {
const mockFile = new File(['dummy'], 'flyer.jpg', { type: 'image/jpeg' });
const mockFile = new File(['dummy image content'], 'flyer.jpg', { type: 'image/jpeg' });
console.log(`\n--- [TEST START] extractAddressFromImage ---`);
await aiApiClient.extractAddressFromImage(mockFile, 'test-token');
@@ -209,7 +215,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('extractLogoFromImage', () => {
it('should construct FormData and send a POST request', async () => {
const mockFile = new File(['logo'], 'logo.jpg', { type: 'image/jpeg' });
const mockFile = new File(['dummy image content'], 'logo.jpg', { type: 'image/jpeg' });
console.log(`\n--- [TEST START] extractLogoFromImage ---`);
await aiApiClient.extractLogoFromImage([mockFile], 'test-token');
@@ -345,7 +351,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
describe('rescanImageArea', () => {
it('should construct FormData with image, cropArea, and extractionType', async () => {
const mockFile = new File(['dummy-content'], 'flyer-page.jpg', { type: 'image/jpeg' });
const mockFile = new File(['dummy image content'], 'flyer-page.jpg', { type: 'image/jpeg' });
const cropArea = { x: 10, y: 20, width: 100, height: 50 };
const extractionType = 'item_details' as const;
console.log(`\n--- [TEST START] rescanImageArea ---`);