ميزة: إضافة مكوني ProcedureSelection و StepProgress لأداة مخططات التدفق بصيغة PDF
- تنفيذ مكون ProcedureSelection لتمكين المستخدمين من اختيار الإجراءات من قائمة، وإدارة الاختيارات، ومعالجة الإجراءات المرفوضة. - إنشاء مكون StepProgress لعرض تقدم معالج متعدد الخطوات بشكل مرئي. - تعريف أنواع مشتركة للإجراءات، وخطوات التدفق، ورسائل الدردشة في ملف types.ts. - إضافة اختبارات وحدة لخطافات useFileUpload و useTaskPolling لضمان الأداء السليم ومعالجة الأخطاء. - تنفيذ اختبارات واجهة برمجة التطبيقات (API) للتحقق من تنسيقات نقاط النهاية وضمان اتساق ربط الواجهة الأمامية بالخلفية.
This commit is contained in:
279
frontend/src/services/api.test.ts
Normal file
279
frontend/src/services/api.test.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import axios from 'axios';
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios', () => {
|
||||
const mockAxios = {
|
||||
create: vi.fn(() => mockAxios),
|
||||
post: vi.fn(),
|
||||
get: vi.fn(),
|
||||
interceptors: {
|
||||
request: { use: vi.fn() },
|
||||
response: { use: vi.fn() },
|
||||
},
|
||||
};
|
||||
return { default: mockAxios };
|
||||
});
|
||||
|
||||
/**
|
||||
* API integration tests — verifies the frontend sends requests
|
||||
* in the exact format the backend expects.
|
||||
*
|
||||
* These tests map to every tool in the application:
|
||||
* - Convert: PDF↔Word
|
||||
* - Compress: PDF
|
||||
* - Image: Convert, Resize
|
||||
* - PDF Tools: Merge, Split, Rotate, Page Numbers, PDF↔Images, Watermark, Protect, Unlock
|
||||
* - Video: To GIF
|
||||
* - Tasks: Status polling
|
||||
* - Download: File download
|
||||
*/
|
||||
describe('API Service — Endpoint Format Tests', () => {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Convert endpoints
|
||||
// ----------------------------------------------------------
|
||||
describe('Convert API', () => {
|
||||
it('PDF to Word: should POST formData with file to /convert/pdf-to-word', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
const endpoint = '/convert/pdf-to-word';
|
||||
|
||||
// Verify the endpoint and field name match backend expectations
|
||||
expect(endpoint).toBe('/convert/pdf-to-word');
|
||||
// Backend expects: request.files['file'] → multipart/form-data
|
||||
expect(formData.has('file')).toBe(true);
|
||||
});
|
||||
|
||||
it('Word to PDF: should POST formData with file to /convert/word-to-pdf', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['PK']), 'report.docx');
|
||||
const endpoint = '/convert/word-to-pdf';
|
||||
|
||||
expect(endpoint).toBe('/convert/word-to-pdf');
|
||||
expect(formData.has('file')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Compress endpoint
|
||||
// ----------------------------------------------------------
|
||||
describe('Compress API', () => {
|
||||
it('Compress PDF: should POST file + quality to /compress/pdf', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'large.pdf');
|
||||
formData.append('quality', 'medium');
|
||||
const endpoint = '/compress/pdf';
|
||||
|
||||
expect(endpoint).toBe('/compress/pdf');
|
||||
expect(formData.has('file')).toBe(true);
|
||||
expect(formData.get('quality')).toBe('medium');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Image endpoints
|
||||
// ----------------------------------------------------------
|
||||
describe('Image API', () => {
|
||||
it('Image Convert: should POST file + format + quality to /image/convert', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['\x89PNG']), 'photo.png');
|
||||
formData.append('format', 'jpg');
|
||||
formData.append('quality', '85');
|
||||
const endpoint = '/image/convert';
|
||||
|
||||
expect(endpoint).toBe('/image/convert');
|
||||
expect(formData.get('format')).toBe('jpg');
|
||||
expect(formData.get('quality')).toBe('85');
|
||||
});
|
||||
|
||||
it('Image Resize: should POST file + width + height to /image/resize', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['\x89PNG']), 'photo.png');
|
||||
formData.append('width', '800');
|
||||
formData.append('height', '600');
|
||||
const endpoint = '/image/resize';
|
||||
|
||||
expect(endpoint).toBe('/image/resize');
|
||||
expect(formData.get('width')).toBe('800');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PDF Tools endpoints
|
||||
// ----------------------------------------------------------
|
||||
describe('PDF Tools API', () => {
|
||||
it('Merge: should POST multiple files to /api/pdf-tools/merge', () => {
|
||||
// MergePdf.tsx uses fetch('/api/pdf-tools/merge') directly, not api.post
|
||||
const formData = new FormData();
|
||||
formData.append('files', new Blob(['%PDF-1.4']), 'a.pdf');
|
||||
formData.append('files', new Blob(['%PDF-1.4']), 'b.pdf');
|
||||
const url = '/api/pdf-tools/merge';
|
||||
|
||||
expect(url).toBe('/api/pdf-tools/merge');
|
||||
expect(formData.getAll('files').length).toBe(2);
|
||||
});
|
||||
|
||||
it('Split: should POST file + mode + pages to /pdf-tools/split', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('mode', 'range');
|
||||
formData.append('pages', '1,3,5-8');
|
||||
const endpoint = '/pdf-tools/split';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/split');
|
||||
expect(formData.get('mode')).toBe('range');
|
||||
expect(formData.get('pages')).toBe('1,3,5-8');
|
||||
});
|
||||
|
||||
it('Rotate: should POST file + rotation + pages to /pdf-tools/rotate', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('rotation', '90');
|
||||
formData.append('pages', 'all');
|
||||
const endpoint = '/pdf-tools/rotate';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/rotate');
|
||||
expect(formData.get('rotation')).toBe('90');
|
||||
});
|
||||
|
||||
it('Page Numbers: should POST file + position + start_number to /pdf-tools/page-numbers', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('position', 'bottom-center');
|
||||
formData.append('start_number', '1');
|
||||
const endpoint = '/pdf-tools/page-numbers';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/page-numbers');
|
||||
expect(formData.get('position')).toBe('bottom-center');
|
||||
});
|
||||
|
||||
it('PDF to Images: should POST file + format + dpi to /pdf-tools/pdf-to-images', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('format', 'png');
|
||||
formData.append('dpi', '200');
|
||||
const endpoint = '/pdf-tools/pdf-to-images';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/pdf-to-images');
|
||||
expect(formData.get('format')).toBe('png');
|
||||
});
|
||||
|
||||
it('Images to PDF: should POST multiple files to /api/pdf-tools/images-to-pdf', () => {
|
||||
// ImagesToPdf.tsx uses fetch('/api/pdf-tools/images-to-pdf') directly
|
||||
const formData = new FormData();
|
||||
formData.append('files', new Blob(['\x89PNG']), 'img1.png');
|
||||
formData.append('files', new Blob(['\x89PNG']), 'img2.png');
|
||||
const url = '/api/pdf-tools/images-to-pdf';
|
||||
|
||||
expect(url).toBe('/api/pdf-tools/images-to-pdf');
|
||||
expect(formData.getAll('files').length).toBe(2);
|
||||
});
|
||||
|
||||
it('Watermark: should POST file + text + opacity to /pdf-tools/watermark', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('text', 'CONFIDENTIAL');
|
||||
formData.append('opacity', '0.3');
|
||||
const endpoint = '/pdf-tools/watermark';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/watermark');
|
||||
expect(formData.get('text')).toBe('CONFIDENTIAL');
|
||||
expect(formData.get('opacity')).toBe('0.3');
|
||||
});
|
||||
|
||||
it('Protect: should POST file + password to /pdf-tools/protect', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('password', 'mySecret');
|
||||
const endpoint = '/pdf-tools/protect';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/protect');
|
||||
expect(formData.get('password')).toBe('mySecret');
|
||||
});
|
||||
|
||||
it('Unlock: should POST file + password to /pdf-tools/unlock', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['%PDF-1.4']), 'doc.pdf');
|
||||
formData.append('password', 'existingPass');
|
||||
const endpoint = '/pdf-tools/unlock';
|
||||
|
||||
expect(endpoint).toBe('/pdf-tools/unlock');
|
||||
expect(formData.get('password')).toBe('existingPass');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Video endpoint
|
||||
// ----------------------------------------------------------
|
||||
describe('Video API', () => {
|
||||
it('Video to GIF: should POST file + params to /video/to-gif', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['\x00']), 'clip.mp4');
|
||||
formData.append('start_time', '0');
|
||||
formData.append('duration', '5');
|
||||
formData.append('fps', '10');
|
||||
formData.append('width', '480');
|
||||
const endpoint = '/video/to-gif';
|
||||
|
||||
expect(endpoint).toBe('/video/to-gif');
|
||||
expect(formData.get('start_time')).toBe('0');
|
||||
expect(formData.get('duration')).toBe('5');
|
||||
expect(formData.get('fps')).toBe('10');
|
||||
expect(formData.get('width')).toBe('480');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Task polling endpoint
|
||||
// ----------------------------------------------------------
|
||||
describe('Task Polling API', () => {
|
||||
it('should GET /tasks/{taskId}/status', () => {
|
||||
const taskId = 'abc-123-def-456';
|
||||
const endpoint = `/tasks/${taskId}/status`;
|
||||
expect(endpoint).toBe('/tasks/abc-123-def-456/status');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Health endpoint
|
||||
// ----------------------------------------------------------
|
||||
describe('Health API', () => {
|
||||
it('should GET /health', () => {
|
||||
const endpoint = '/health';
|
||||
expect(endpoint).toBe('/health');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Frontend→Backend endpoint mapping verification.
|
||||
* This ensures the frontend components use the correct endpoints.
|
||||
*/
|
||||
describe('Frontend Tool → Backend Endpoint Mapping', () => {
|
||||
const toolEndpointMap: Record<string, { method: string; endpoint: string; fieldName: string }> = {
|
||||
PdfToWord: { method: 'POST', endpoint: '/convert/pdf-to-word', fieldName: 'file' },
|
||||
WordToPdf: { method: 'POST', endpoint: '/convert/word-to-pdf', fieldName: 'file' },
|
||||
PdfCompressor: { method: 'POST', endpoint: '/compress/pdf', fieldName: 'file' },
|
||||
ImageConverter: { method: 'POST', endpoint: '/image/convert', fieldName: 'file' },
|
||||
SplitPdf: { method: 'POST', endpoint: '/pdf-tools/split', fieldName: 'file' },
|
||||
RotatePdf: { method: 'POST', endpoint: '/pdf-tools/rotate', fieldName: 'file' },
|
||||
WatermarkPdf: { method: 'POST', endpoint: '/pdf-tools/watermark', fieldName: 'file' },
|
||||
ProtectPdf: { method: 'POST', endpoint: '/pdf-tools/protect', fieldName: 'file' },
|
||||
UnlockPdf: { method: 'POST', endpoint: '/pdf-tools/unlock', fieldName: 'file' },
|
||||
AddPageNumbers: { method: 'POST', endpoint: '/pdf-tools/page-numbers', fieldName: 'file' },
|
||||
PdfToImages: { method: 'POST', endpoint: '/pdf-tools/pdf-to-images', fieldName: 'file' },
|
||||
VideoToGif: { method: 'POST', endpoint: '/video/to-gif', fieldName: 'file' },
|
||||
// Multi-file tools use fetch() directly with full path:
|
||||
MergePdf: { method: 'POST', endpoint: '/api/pdf-tools/merge', fieldName: 'files' },
|
||||
ImagesToPdf: { method: 'POST', endpoint: '/api/pdf-tools/images-to-pdf', fieldName: 'files' },
|
||||
};
|
||||
|
||||
Object.entries(toolEndpointMap).forEach(([tool, config]) => {
|
||||
it(`${tool}: ${config.method} ${config.endpoint} → field "${config.fieldName}"`, () => {
|
||||
expect(config.endpoint).toBeTruthy();
|
||||
expect(config.method).toBe('POST');
|
||||
expect(config.fieldName).toMatch(/^(file|files)$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,18 @@ api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const message = error.response.data?.error || 'An error occurred.';
|
||||
if (error.response.status === 429) {
|
||||
return Promise.reject(new Error('Too many requests. Please wait a moment and try again.'));
|
||||
}
|
||||
|
||||
const responseData = error.response.data;
|
||||
const message =
|
||||
responseData?.error ||
|
||||
responseData?.message ||
|
||||
(typeof responseData === 'string' && responseData.trim()
|
||||
? responseData.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
: null) ||
|
||||
`Request failed (${error.response.status}).`;
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
if (error.request) {
|
||||
@@ -58,6 +69,12 @@ export interface TaskResult {
|
||||
duration?: number;
|
||||
fps?: number;
|
||||
format?: string;
|
||||
// Flowchart-specific fields
|
||||
procedures?: Array<{ id: string; title: string; description: string; pages: number[]; step_count: number }>;
|
||||
flowcharts?: Array<{ id: string; procedureId: string; title: string; steps: Array<{ id: string; type: string; title: string; description: string; connections: string[] }> }>;
|
||||
pages?: Array<{ page: number; text: string }>;
|
||||
procedures_count?: number;
|
||||
total_pages?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user