mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Bugfix/V2/remove-timeout-on-fetch (#4510)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
parent
0bdc6466ca
commit
7d44cc1a40
@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import axios from 'axios';
|
import apiClient from '../../../services/apiClient';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
||||||
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||||
@ -108,7 +108,7 @@ export const convertProcessor = async (
|
|||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
try {
|
try {
|
||||||
const formData = buildConvertFormData(parameters, [file]);
|
const formData = buildConvertFormData(parameters, [file]);
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await apiClient.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export const convertProcessor = async (
|
|||||||
} else {
|
} else {
|
||||||
// Batch processing for simple cases (image→PDF combine)
|
// Batch processing for simple cases (image→PDF combine)
|
||||||
const formData = buildConvertFormData(parameters, selectedFiles);
|
const formData = buildConvertFormData(parameters, selectedFiles);
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await apiClient.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
const baseFilename = selectedFiles.length === 1
|
const baseFilename = selectedFiles.length === 1
|
||||||
? selectedFiles[0].name
|
? selectedFiles[0].name
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import axios, { CancelTokenSource } from '../../../services/http';
|
import axios, {type CancelTokenSource} from 'axios'; // Real axios for static methods (CancelToken, isCancel)
|
||||||
|
import apiClient from '../../../services/apiClient'; // Our configured instance
|
||||||
import { processResponse, ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { processResponse, ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
import { isEmptyOutput } from '../../../services/errorUtils';
|
import { isEmptyOutput } from '../../../services/errorUtils';
|
||||||
import type { ProcessingProgress } from './useToolState';
|
import type { ProcessingProgress } from './useToolState';
|
||||||
@ -42,9 +43,9 @@ export const useToolApiCalls = <TParams = void>() => {
|
|||||||
const formData = config.buildFormData(params, file);
|
const formData = config.buildFormData(params, file);
|
||||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||||
console.debug('[processFiles] POST', { endpoint, name: file.name });
|
console.debug('[processFiles] POST', { endpoint, name: file.name });
|
||||||
const response = await axios.post(endpoint, formData, {
|
const response = await apiClient.post(endpoint, formData, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
cancelToken: cancelTokenRef.current.token,
|
cancelToken: cancelTokenRef.current?.token,
|
||||||
});
|
});
|
||||||
console.debug('[processFiles] Response OK', { name: file.name, status: (response as any)?.status });
|
console.debug('[processFiles] Response OK', { name: file.name, status: (response as any)?.status });
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useRef, useEffect } from 'react';
|
import { useCallback, useRef, useEffect } from 'react';
|
||||||
import axios from '../../../services/http';
|
import apiClient from '../../../services/apiClient';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFileContext } from '../../../contexts/FileContext';
|
import { useFileContext } from '../../../contexts/FileContext';
|
||||||
import { useToolState, type ProcessingProgress } from './useToolState';
|
import { useToolState, type ProcessingProgress } from './useToolState';
|
||||||
@ -243,7 +243,7 @@ export const useToolOperation = <TParams>(
|
|||||||
const formData = config.buildFormData(params, filesForAPI);
|
const formData = config.buildFormData(params, filesForAPI);
|
||||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||||
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await apiClient.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||||
if (config.responseHandler) {
|
if (config.responseHandler) {
|
||||||
|
22
frontend/src/services/apiClient.ts
Normal file
22
frontend/src/services/apiClient.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// frontend/src/services/http.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
import { handleHttpError } from './httpErrorHandler';
|
||||||
|
|
||||||
|
// Create axios instance with default config
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/', // Use env var or relative path (proxied by Vite in dev)
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- Install error interceptor ----------
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
await handleHttpError(error); // Handle error (shows toast unless suppressed)
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- Exports ----------
|
||||||
|
export default apiClient;
|
@ -1,255 +0,0 @@
|
|||||||
// frontend/src/services/http.ts
|
|
||||||
import axios from 'axios';
|
|
||||||
import type { AxiosInstance } from 'axios';
|
|
||||||
import { alert } from '../components/toast';
|
|
||||||
import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } from './errorUtils';
|
|
||||||
import { showSpecialErrorToast } from './specialErrorToasts';
|
|
||||||
|
|
||||||
const FRIENDLY_FALLBACK = 'There was an error processing your request.';
|
|
||||||
const MAX_TOAST_BODY_CHARS = 400; // avoid massive, unreadable toasts
|
|
||||||
|
|
||||||
function clampText(s: string, max = MAX_TOAST_BODY_CHARS): string {
|
|
||||||
return s && s.length > max ? `${s.slice(0, max)}…` : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUnhelpfulMessage(msg: string | null | undefined): boolean {
|
|
||||||
const s = (msg || '').trim();
|
|
||||||
if (!s) return true;
|
|
||||||
// Common unhelpful payloads we see
|
|
||||||
if (s === '{}' || s === '[]') return true;
|
|
||||||
if (/^request failed/i.test(s)) return true;
|
|
||||||
if (/^network error/i.test(s)) return true;
|
|
||||||
if (/^[45]\d\d\b/.test(s)) return true; // "500 Server Error" etc.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleForStatus(status?: number): string {
|
|
||||||
if (!status) return 'Network error';
|
|
||||||
if (status >= 500) return 'Server error';
|
|
||||||
if (status >= 400) return 'Request error';
|
|
||||||
return 'Request failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const status = error.response?.status;
|
|
||||||
const _statusText = error.response?.statusText || '';
|
|
||||||
let parsed: any = undefined;
|
|
||||||
const raw = error.response?.data;
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
try { parsed = JSON.parse(raw); } catch { /* keep as string */ }
|
|
||||||
} else {
|
|
||||||
parsed = raw;
|
|
||||||
}
|
|
||||||
const extractIds = (): string[] | undefined => {
|
|
||||||
if (Array.isArray(parsed?.errorFileIds)) return parsed.errorFileIds as string[];
|
|
||||||
const rawText = typeof raw === 'string' ? raw : '';
|
|
||||||
const uuidMatches = rawText.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g);
|
|
||||||
return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const body = ((): string => {
|
|
||||||
const data = parsed;
|
|
||||||
if (!data) return typeof raw === 'string' ? raw : '';
|
|
||||||
const ids = extractIds();
|
|
||||||
if (ids && ids.length > 0) return `Failed files: ${ids.join(', ')}`;
|
|
||||||
if (data?.message) return data.message as string;
|
|
||||||
if (typeof raw === 'string') return raw;
|
|
||||||
try { return JSON.stringify(data); } catch { return ''; }
|
|
||||||
})();
|
|
||||||
const ids = extractIds();
|
|
||||||
const title = titleForStatus(status);
|
|
||||||
if (ids && ids.length > 0) {
|
|
||||||
return { title, body: 'Process failed due to invalid/corrupted file(s)' };
|
|
||||||
}
|
|
||||||
if (status === 422) {
|
|
||||||
const fallbackMsg = 'Process failed due to invalid/corrupted file(s)';
|
|
||||||
const bodyMsg = isUnhelpfulMessage(body) ? fallbackMsg : body;
|
|
||||||
return { title, body: bodyMsg };
|
|
||||||
}
|
|
||||||
const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body;
|
|
||||||
return { title, body: bodyMsg };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const msg = (error?.message || String(error)) as string;
|
|
||||||
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
|
||||||
} catch (e) {
|
|
||||||
// ignore extraction errors
|
|
||||||
console.debug('extractAxiosErrorMessage', e);
|
|
||||||
return { title: 'Network error', body: FRIENDLY_FALLBACK };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Axios instance creation ----------
|
|
||||||
const __globalAny = (typeof window !== 'undefined' ? (window as any) : undefined);
|
|
||||||
|
|
||||||
type ExtendedAxiosInstance = AxiosInstance & {
|
|
||||||
CancelToken: typeof axios.CancelToken;
|
|
||||||
isCancel: typeof axios.isCancel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const __PREV_CLIENT: ExtendedAxiosInstance | undefined =
|
|
||||||
__globalAny?.__SPDF_HTTP_CLIENT as ExtendedAxiosInstance | undefined;
|
|
||||||
|
|
||||||
let __createdClient: any;
|
|
||||||
if (__PREV_CLIENT) {
|
|
||||||
__createdClient = __PREV_CLIENT;
|
|
||||||
} else if (typeof (axios as any)?.create === 'function') {
|
|
||||||
try {
|
|
||||||
__createdClient = (axios as any).create();
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('createClient', e);
|
|
||||||
__createdClient = axios as any;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
__createdClient = axios as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiClient: ExtendedAxiosInstance = (__createdClient || (axios as any)) as ExtendedAxiosInstance;
|
|
||||||
|
|
||||||
// Augment instance with axios static helpers for backwards compatibility
|
|
||||||
if (apiClient) {
|
|
||||||
try { (apiClient as any).CancelToken = (axios as any).CancelToken; } catch (e) { console.debug('setCancelToken', e); }
|
|
||||||
try { (apiClient as any).isCancel = (axios as any).isCancel; } catch (e) { console.debug('setIsCancel', e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Base defaults ----------
|
|
||||||
try {
|
|
||||||
const env = (import.meta as any)?.env || {};
|
|
||||||
apiClient.defaults.baseURL = env?.VITE_API_BASE_URL ?? '/';
|
|
||||||
apiClient.defaults.responseType = 'json';
|
|
||||||
// If OSS relies on cookies, uncomment:
|
|
||||||
// apiClient.defaults.withCredentials = true;
|
|
||||||
// Sensible timeout to avoid “forever hanging”:
|
|
||||||
apiClient.defaults.timeout = 20000;
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('setDefaults', e);
|
|
||||||
apiClient.defaults.baseURL = apiClient.defaults.baseURL || '/';
|
|
||||||
apiClient.defaults.responseType = apiClient.defaults.responseType || 'json';
|
|
||||||
apiClient.defaults.timeout = apiClient.defaults.timeout || 20000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Install a single response error interceptor (dedup + UX) ----------
|
|
||||||
if (__globalAny?.__SPDF_HTTP_ERR_INTERCEPTOR_ID !== undefined && __PREV_CLIENT) {
|
|
||||||
try {
|
|
||||||
__PREV_CLIENT.interceptors.response.eject(__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID);
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('ejectInterceptor', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const __recentSpecialByEndpoint: Record<string, number> = (__globalAny?.__SPDF_RECENT_SPECIAL || {});
|
|
||||||
const __SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
|
|
||||||
|
|
||||||
const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
|
||||||
? apiClient.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error) => {
|
|
||||||
// Compute title/body (friendly) from the error object
|
|
||||||
const { title, body } = extractAxiosErrorMessage(error);
|
|
||||||
|
|
||||||
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
|
||||||
const raw = (error?.response?.data) as any;
|
|
||||||
let normalized: unknown = raw;
|
|
||||||
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
|
||||||
|
|
||||||
// 1) If server sends structured file IDs for failures, also mark them errored in UI
|
|
||||||
try {
|
|
||||||
const ids = extractErrorFileIds(normalized);
|
|
||||||
if (ids && ids.length > 0) {
|
|
||||||
broadcastErroredFiles(ids);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('extractErrorFileIds', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Generic-vs-special dedupe by endpoint
|
|
||||||
const url: string | undefined = error?.config?.url;
|
|
||||||
const status: number | undefined = error?.response?.status;
|
|
||||||
const now = Date.now();
|
|
||||||
const isSpecial =
|
|
||||||
status === 422 ||
|
|
||||||
status === 409 || // often actionable conflicts
|
|
||||||
/Failed files:/.test(body) ||
|
|
||||||
/invalid\/corrupted file\(s\)/i.test(body);
|
|
||||||
|
|
||||||
if (isSpecial && url) {
|
|
||||||
__recentSpecialByEndpoint[url] = now;
|
|
||||||
if (__globalAny) __globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
|
|
||||||
}
|
|
||||||
if (!isSpecial && url) {
|
|
||||||
const last = __recentSpecialByEndpoint[url] || 0;
|
|
||||||
if (now - last < __SPECIAL_SUPPRESS_MS) {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Show specialized friendly toasts if matched; otherwise show the generic one
|
|
||||||
let rawString: string | undefined;
|
|
||||||
try {
|
|
||||||
rawString =
|
|
||||||
typeof normalized === 'string'
|
|
||||||
? normalized
|
|
||||||
: JSON.stringify(normalized);
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('extractErrorFileIds', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handled = showSpecialErrorToast(rawString, { status });
|
|
||||||
if (!handled) {
|
|
||||||
const displayBody = clampText(body);
|
|
||||||
alert({ alertType: 'error', title, body: displayBody, expandable: true, isPersistentPopup: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: undefined as any;
|
|
||||||
|
|
||||||
if (__globalAny) {
|
|
||||||
__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID = __INTERCEPTOR_ID__;
|
|
||||||
__globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
|
|
||||||
__globalAny.__SPDF_HTTP_CLIENT = apiClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Fetch helper ----------
|
|
||||||
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
||||||
const res = await fetch(input, { credentials: init?.credentials ?? 'include', ...init });
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
let detail = '';
|
|
||||||
try {
|
|
||||||
const ct = res.headers.get('content-type') || '';
|
|
||||||
if (ct.includes('application/json')) {
|
|
||||||
const data = await res.json();
|
|
||||||
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
|
|
||||||
} else {
|
|
||||||
detail = await res.text();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore parse errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = titleForStatus(res.status);
|
|
||||||
const body = isUnhelpfulMessage(detail || res.statusText) ? FRIENDLY_FALLBACK : (detail || res.statusText);
|
|
||||||
alert({ alertType: 'error', title, body: clampText(body), expandable: true, isPersistentPopup: false });
|
|
||||||
|
|
||||||
// Important: match Axios semantics so callers can try/catch
|
|
||||||
throw new Error(body || res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Convenience API surface and exports ----------
|
|
||||||
export const api = {
|
|
||||||
get: apiClient.get,
|
|
||||||
post: apiClient.post,
|
|
||||||
put: apiClient.put,
|
|
||||||
patch: apiClient.patch,
|
|
||||||
delete: apiClient.delete,
|
|
||||||
request: apiClient.request,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apiClient;
|
|
||||||
export type { CancelTokenSource } from 'axios';
|
|
147
frontend/src/services/httpErrorHandler.ts
Normal file
147
frontend/src/services/httpErrorHandler.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// frontend/src/services/httpErrorHandler.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
import { alert } from '../components/toast';
|
||||||
|
import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } from './errorUtils';
|
||||||
|
import { showSpecialErrorToast } from './specialErrorToasts';
|
||||||
|
|
||||||
|
const FRIENDLY_FALLBACK = 'There was an error processing your request.';
|
||||||
|
const MAX_TOAST_BODY_CHARS = 400; // avoid massive, unreadable toasts
|
||||||
|
|
||||||
|
function clampText(s: string, max = MAX_TOAST_BODY_CHARS): string {
|
||||||
|
return s && s.length > max ? `${s.slice(0, max)}…` : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnhelpfulMessage(msg: string | null | undefined): boolean {
|
||||||
|
const s = (msg || '').trim();
|
||||||
|
if (!s) return true;
|
||||||
|
// Common unhelpful payloads we see
|
||||||
|
if (s === '{}' || s === '[]') return true;
|
||||||
|
if (/^request failed/i.test(s)) return true;
|
||||||
|
if (/^network error/i.test(s)) return true;
|
||||||
|
if (/^[45]\d\d\b/.test(s)) return true; // "500 Server Error" etc.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleForStatus(status?: number): string {
|
||||||
|
if (!status) return 'Network error';
|
||||||
|
if (status >= 500) return 'Server error';
|
||||||
|
if (status >= 400) return 'Request error';
|
||||||
|
return 'Request failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const _statusText = error.response?.statusText || '';
|
||||||
|
let parsed: any = undefined;
|
||||||
|
const raw = error.response?.data;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try { parsed = JSON.parse(raw); } catch { /* keep as string */ }
|
||||||
|
} else {
|
||||||
|
parsed = raw;
|
||||||
|
}
|
||||||
|
const extractIds = (): string[] | undefined => {
|
||||||
|
if (Array.isArray(parsed?.errorFileIds)) return parsed.errorFileIds as string[];
|
||||||
|
const rawText = typeof raw === 'string' ? raw : '';
|
||||||
|
const uuidMatches = rawText.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g);
|
||||||
|
return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = ((): string => {
|
||||||
|
const data = parsed;
|
||||||
|
if (!data) return typeof raw === 'string' ? raw : '';
|
||||||
|
const ids = extractIds();
|
||||||
|
if (ids && ids.length > 0) return `Failed files: ${ids.join(', ')}`;
|
||||||
|
if (data?.message) return data.message as string;
|
||||||
|
if (typeof raw === 'string') return raw;
|
||||||
|
try { return JSON.stringify(data); } catch { return ''; }
|
||||||
|
})();
|
||||||
|
const ids = extractIds();
|
||||||
|
const title = titleForStatus(status);
|
||||||
|
if (ids && ids.length > 0) {
|
||||||
|
return { title, body: 'Process failed due to invalid/corrupted file(s)' };
|
||||||
|
}
|
||||||
|
if (status === 422) {
|
||||||
|
const fallbackMsg = 'Process failed due to invalid/corrupted file(s)';
|
||||||
|
const bodyMsg = isUnhelpfulMessage(body) ? fallbackMsg : body;
|
||||||
|
return { title, body: bodyMsg };
|
||||||
|
}
|
||||||
|
const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body;
|
||||||
|
return { title, body: bodyMsg };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const msg = (error?.message || String(error)) as string;
|
||||||
|
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
||||||
|
} catch (e) {
|
||||||
|
// ignore extraction errors
|
||||||
|
console.debug('extractAxiosErrorMessage', e);
|
||||||
|
return { title: 'Network error', body: FRIENDLY_FALLBACK };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-scoped state to reduce global variable usage
|
||||||
|
const recentSpecialByEndpoint: Record<string, number> = {};
|
||||||
|
const SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles HTTP errors with toast notifications and file error broadcasting
|
||||||
|
* Returns true if the error should be suppressed (deduplicated), false otherwise
|
||||||
|
*/
|
||||||
|
export async function handleHttpError(error: any): Promise<boolean> {
|
||||||
|
// Compute title/body (friendly) from the error object
|
||||||
|
const { title, body } = extractAxiosErrorMessage(error);
|
||||||
|
|
||||||
|
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
||||||
|
const raw = (error?.response?.data) as any;
|
||||||
|
let normalized: unknown = raw;
|
||||||
|
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
||||||
|
|
||||||
|
// 1) If server sends structured file IDs for failures, also mark them errored in UI
|
||||||
|
try {
|
||||||
|
const ids = extractErrorFileIds(normalized);
|
||||||
|
if (ids && ids.length > 0) {
|
||||||
|
broadcastErroredFiles(ids);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug('extractErrorFileIds', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Generic-vs-special dedupe by endpoint
|
||||||
|
const url: string | undefined = error?.config?.url;
|
||||||
|
const status: number | undefined = error?.response?.status;
|
||||||
|
const now = Date.now();
|
||||||
|
const isSpecial =
|
||||||
|
status === 422 ||
|
||||||
|
status === 409 || // often actionable conflicts
|
||||||
|
/Failed files:/.test(body) ||
|
||||||
|
/invalid\/corrupted file\(s\)/i.test(body);
|
||||||
|
|
||||||
|
if (isSpecial && url) {
|
||||||
|
recentSpecialByEndpoint[url] = now;
|
||||||
|
}
|
||||||
|
if (!isSpecial && url) {
|
||||||
|
const last = recentSpecialByEndpoint[url] || 0;
|
||||||
|
if (now - last < SPECIAL_SUPPRESS_MS) {
|
||||||
|
return true; // Suppress this error (deduplicated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Show specialized friendly toasts if matched; otherwise show the generic one
|
||||||
|
let rawString: string | undefined;
|
||||||
|
try {
|
||||||
|
rawString =
|
||||||
|
typeof normalized === 'string'
|
||||||
|
? normalized
|
||||||
|
: JSON.stringify(normalized);
|
||||||
|
} catch (e) {
|
||||||
|
console.debug('extractErrorFileIds', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handled = showSpecialErrorToast(rawString, { status });
|
||||||
|
if (!handled) {
|
||||||
|
const displayBody = clampText(body);
|
||||||
|
alert({ alertType: 'error', title, body: displayBody, expandable: true, isPersistentPopup: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Error was handled with toast, continue normal rejection
|
||||||
|
}
|
@ -17,13 +17,40 @@ import { ConvertParameters } from '../../hooks/tools/convert/useConvertParameter
|
|||||||
import { FileContextProvider } from '../../contexts/FileContext';
|
import { FileContextProvider } from '../../contexts/FileContext';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../../i18n/config';
|
import i18n from '../../i18n/config';
|
||||||
import axios from 'axios';
|
|
||||||
import { createTestStirlingFile } from '../utils/testFileHelpers';
|
import { createTestStirlingFile } from '../utils/testFileHelpers';
|
||||||
import { StirlingFile } from '../../types/fileContext';
|
import { StirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios (for static methods like CancelToken, isCancel)
|
||||||
vi.mock('axios');
|
vi.mock('axios', () => ({
|
||||||
const mockedAxios = vi.mocked(axios);
|
default: {
|
||||||
|
CancelToken: {
|
||||||
|
source: vi.fn(() => ({
|
||||||
|
token: 'mock-cancel-token',
|
||||||
|
cancel: vi.fn()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
isCancel: vi.fn(() => false),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock our apiClient service
|
||||||
|
vi.mock('../../services/apiClient', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: {
|
||||||
|
response: {
|
||||||
|
use: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the mocked apiClient
|
||||||
|
import apiClient from '../../services/apiClient';
|
||||||
|
const mockedApiClient = vi.mocked(apiClient);
|
||||||
|
|
||||||
// Mock only essential services that are actually called by the tests
|
// Mock only essential services that are actually called by the tests
|
||||||
vi.mock('../../services/fileStorage', () => ({
|
vi.mock('../../services/fileStorage', () => ({
|
||||||
@ -71,8 +98,8 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Setup default axios mock
|
// Setup default apiClient mock
|
||||||
mockedAxios.post = vi.fn();
|
mockedApiClient.post = vi.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -83,7 +110,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should make correct API call for PDF to PNG conversion', async () => {
|
test('should make correct API call for PDF to PNG conversion', async () => {
|
||||||
const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' });
|
const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({
|
||||||
data: mockBlob,
|
data: mockBlob,
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'OK'
|
statusText: 'OK'
|
||||||
@ -126,14 +153,14 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify axios was called with correct parameters
|
// Verify axios was called with correct parameters
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/convert/pdf/img',
|
'/api/v1/convert/pdf/img',
|
||||||
expect.any(FormData),
|
expect.any(FormData),
|
||||||
{ responseType: 'blob' }
|
{ responseType: 'blob' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify FormData contains correct parameters
|
// Verify FormData contains correct parameters
|
||||||
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formDataCall = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formDataCall.get('imageFormat')).toBe('png');
|
expect(formDataCall.get('imageFormat')).toBe('png');
|
||||||
expect(formDataCall.get('colorType')).toBe('color');
|
expect(formDataCall.get('colorType')).toBe('color');
|
||||||
expect(formDataCall.get('dpi')).toBe('300');
|
expect(formDataCall.get('dpi')).toBe('300');
|
||||||
@ -148,7 +175,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should handle API error responses correctly', async () => {
|
test('should handle API error responses correctly', async () => {
|
||||||
const errorMessage = 'Invalid file format';
|
const errorMessage = 'Invalid file format';
|
||||||
(mockedAxios.post as Mock).mockRejectedValueOnce({
|
(mockedApiClient.post as Mock).mockRejectedValueOnce({
|
||||||
response: {
|
response: {
|
||||||
status: 400,
|
status: 400,
|
||||||
data: errorMessage
|
data: errorMessage
|
||||||
@ -199,7 +226,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle network errors gracefully', async () => {
|
test('should handle network errors gracefully', async () => {
|
||||||
(mockedAxios.post as Mock).mockRejectedValueOnce(new Error('Network error'));
|
(mockedApiClient.post as Mock).mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
const { result } = renderHook(() => useConvertOperation(), {
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
@ -246,7 +273,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should correctly map image conversion parameters to API call', async () => {
|
test('should correctly map image conversion parameters to API call', async () => {
|
||||||
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
|
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({
|
||||||
data: mockBlob,
|
data: mockBlob,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -292,7 +319,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify integration: hook parameters → FormData → axios call → hook state
|
// Verify integration: hook parameters → FormData → axios call → hook state
|
||||||
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formDataCall = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formDataCall.get('imageFormat')).toBe('jpg');
|
expect(formDataCall.get('imageFormat')).toBe('jpg');
|
||||||
expect(formDataCall.get('colorType')).toBe('grayscale');
|
expect(formDataCall.get('colorType')).toBe('grayscale');
|
||||||
expect(formDataCall.get('dpi')).toBe('150');
|
expect(formDataCall.get('dpi')).toBe('150');
|
||||||
@ -307,7 +334,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should make correct API call for PDF to CSV conversion with simplified workflow', async () => {
|
test('should make correct API call for PDF to CSV conversion with simplified workflow', async () => {
|
||||||
const mockBlob = new Blob(['fake-csv-data'], { type: 'text/csv' });
|
const mockBlob = new Blob(['fake-csv-data'], { type: 'text/csv' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({
|
||||||
data: mockBlob,
|
data: mockBlob,
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'OK'
|
statusText: 'OK'
|
||||||
@ -350,14 +377,14 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify correct endpoint is called
|
// Verify correct endpoint is called
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||||
'/api/v1/convert/pdf/csv',
|
'/api/v1/convert/pdf/csv',
|
||||||
expect.any(FormData),
|
expect.any(FormData),
|
||||||
{ responseType: 'blob' }
|
{ responseType: 'blob' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify FormData contains correct parameters for simplified CSV conversion
|
// Verify FormData contains correct parameters for simplified CSV conversion
|
||||||
const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formDataCall = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow
|
expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow
|
||||||
expect(formDataCall.get('fileInput')).toBe(testFile);
|
expect(formDataCall.get('fileInput')).toBe(testFile);
|
||||||
|
|
||||||
@ -406,7 +433,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify integration: utils validation prevents API call, hook shows error
|
// Verify integration: utils validation prevents API call, hook shows error
|
||||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
expect(mockedApiClient.post).not.toHaveBeenCalled();
|
||||||
expect(result.current.errorMessage).toContain('Unsupported conversion format');
|
expect(result.current.errorMessage).toContain('Unsupported conversion format');
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
expect(result.current.downloadUrl).toBe(null);
|
expect(result.current.downloadUrl).toBe(null);
|
||||||
@ -417,7 +444,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should handle multiple file uploads correctly', async () => {
|
test('should handle multiple file uploads correctly', async () => {
|
||||||
const mockBlob = new Blob(['zip-content'], { type: 'application/zip' });
|
const mockBlob = new Blob(['zip-content'], { type: 'application/zip' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob });
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({ data: mockBlob });
|
||||||
|
|
||||||
const { result } = renderHook(() => useConvertOperation(), {
|
const { result } = renderHook(() => useConvertOperation(), {
|
||||||
wrapper: TestWrapper
|
wrapper: TestWrapper
|
||||||
@ -458,7 +485,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify both files were uploaded
|
// Verify both files were uploaded
|
||||||
const calls = (mockedAxios.post as Mock).mock.calls;
|
const calls = (mockedApiClient.post as Mock).mock.calls;
|
||||||
|
|
||||||
for (let i = 0; i < calls.length; i++) {
|
for (let i = 0; i < calls.length; i++) {
|
||||||
const formData = calls[i][1] as FormData;
|
const formData = calls[i][1] as FormData;
|
||||||
@ -506,7 +533,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
await result.current.executeOperation(parameters, []);
|
await result.current.executeOperation(parameters, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
expect(mockedApiClient.post).not.toHaveBeenCalled();
|
||||||
expect(result.current.errorMessage).toContain('noFileSelected');
|
expect(result.current.errorMessage).toContain('noFileSelected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -514,7 +541,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
describe('Error Boundary Integration', () => {
|
describe('Error Boundary Integration', () => {
|
||||||
|
|
||||||
test('should handle corrupted file gracefully', async () => {
|
test('should handle corrupted file gracefully', async () => {
|
||||||
(mockedAxios.post as Mock).mockRejectedValueOnce({
|
(mockedApiClient.post as Mock).mockRejectedValueOnce({
|
||||||
response: {
|
response: {
|
||||||
status: 422,
|
status: 422,
|
||||||
data: 'Processing failed'
|
data: 'Processing failed'
|
||||||
@ -562,7 +589,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle backend service unavailable', async () => {
|
test('should handle backend service unavailable', async () => {
|
||||||
(mockedAxios.post as Mock).mockRejectedValueOnce({
|
(mockedApiClient.post as Mock).mockRejectedValueOnce({
|
||||||
response: {
|
response: {
|
||||||
status: 503,
|
status: 503,
|
||||||
data: 'Service unavailable'
|
data: 'Service unavailable'
|
||||||
@ -614,7 +641,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should record operation in FileContext', async () => {
|
test('should record operation in FileContext', async () => {
|
||||||
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({
|
||||||
data: mockBlob,
|
data: mockBlob,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -667,7 +694,7 @@ describe('Convert Tool Integration Tests', () => {
|
|||||||
|
|
||||||
test('should clean up blob URLs on reset', async () => {
|
test('should clean up blob URLs on reset', async () => {
|
||||||
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
|
||||||
(mockedAxios.post as Mock).mockResolvedValueOnce({
|
(mockedApiClient.post as Mock).mockResolvedValueOnce({
|
||||||
data: mockBlob,
|
data: mockBlob,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -11,14 +11,41 @@ import { useConvertParameters } from '../../hooks/tools/convert/useConvertParame
|
|||||||
import { FileContextProvider } from '../../contexts/FileContext';
|
import { FileContextProvider } from '../../contexts/FileContext';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../../i18n/config';
|
import i18n from '../../i18n/config';
|
||||||
import axios from 'axios';
|
|
||||||
import { detectFileExtension } from '../../utils/fileUtils';
|
import { detectFileExtension } from '../../utils/fileUtils';
|
||||||
import { FIT_OPTIONS } from '../../constants/convertConstants';
|
import { FIT_OPTIONS } from '../../constants/convertConstants';
|
||||||
import { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers';
|
import { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios (for static methods like CancelToken, isCancel)
|
||||||
vi.mock('axios');
|
vi.mock('axios', () => ({
|
||||||
const mockedAxios = vi.mocked(axios);
|
default: {
|
||||||
|
CancelToken: {
|
||||||
|
source: vi.fn(() => ({
|
||||||
|
token: 'mock-cancel-token',
|
||||||
|
cancel: vi.fn()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
isCancel: vi.fn(() => false),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock our apiClient service
|
||||||
|
vi.mock('../../services/apiClient', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: {
|
||||||
|
response: {
|
||||||
|
use: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the mocked apiClient
|
||||||
|
import apiClient from '../../services/apiClient';
|
||||||
|
const mockedApiClient = vi.mocked(apiClient);
|
||||||
|
|
||||||
// Mock only essential services that are actually called by the tests
|
// Mock only essential services that are actually called by the tests
|
||||||
vi.mock('../../services/fileStorage', () => ({
|
vi.mock('../../services/fileStorage', () => ({
|
||||||
@ -61,7 +88,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Mock successful API response
|
// Mock successful API response
|
||||||
(mockedAxios.post as Mock).mockResolvedValue({
|
(mockedApiClient.post as Mock).mockResolvedValue({
|
||||||
data: new Blob(['fake converted content'], { type: 'application/pdf' })
|
data: new Blob(['fake converted content'], { type: 'application/pdf' })
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -103,7 +130,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -139,7 +166,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -183,12 +210,12 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/img/pdf', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/img/pdf', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should send all files in single request
|
// Should send all files in single request
|
||||||
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
const files = formData.getAll('fileInput');
|
const files = formData.getAll('fileInput');
|
||||||
expect(files).toHaveLength(3);
|
expect(files).toHaveLength(3);
|
||||||
});
|
});
|
||||||
@ -229,7 +256,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -269,12 +296,12 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/html/pdf', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/html/pdf', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should process files separately for web files
|
// Should process files separately for web files
|
||||||
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
expect(mockedApiClient.post).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -306,7 +333,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formData.get('zoom')).toBe('1.5');
|
expect(formData.get('zoom')).toBe('1.5');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -340,7 +367,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formData.get('includeAttachments')).toBe('false');
|
expect(formData.get('includeAttachments')).toBe('false');
|
||||||
expect(formData.get('maxAttachmentSizeMB')).toBe('20');
|
expect(formData.get('maxAttachmentSizeMB')).toBe('20');
|
||||||
expect(formData.get('downloadHtml')).toBe('true');
|
expect(formData.get('downloadHtml')).toBe('true');
|
||||||
@ -374,9 +401,9 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formData.get('outputFormat')).toBe('pdfa');
|
expect(formData.get('outputFormat')).toBe('pdfa');
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), {
|
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -418,7 +445,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
|
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
|
||||||
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
|
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
|
||||||
expect(formData.get('colorType')).toBe('grayscale');
|
expect(formData.get('colorType')).toBe('grayscale');
|
||||||
expect(formData.get('autoRotate')).toBe('false');
|
expect(formData.get('autoRotate')).toBe('false');
|
||||||
@ -455,7 +482,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Should make separate API calls for each file
|
// Should make separate API calls for each file
|
||||||
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
expect(mockedApiClient.post).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -472,7 +499,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock one success, one failure
|
// Mock one success, one failure
|
||||||
(mockedAxios.post as Mock)
|
(mockedApiClient.post as Mock)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
data: new Blob(['converted1'], { type: 'application/pdf' })
|
data: new Blob(['converted1'], { type: 'application/pdf' })
|
||||||
})
|
})
|
||||||
@ -498,7 +525,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Should have processed at least one file successfully
|
// Should have processed at least one file successfully
|
||||||
expect(operationResult.current.files.length).toBeGreaterThan(0);
|
expect(operationResult.current.files.length).toBeGreaterThan(0);
|
||||||
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
expect(mockedApiClient.post).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user