ميزة: إضافة مكوني ProcedureSelection و StepProgress لأداة مخططات التدفق بصيغة PDF

- تنفيذ مكون ProcedureSelection لتمكين المستخدمين من اختيار الإجراءات من قائمة، وإدارة الاختيارات، ومعالجة الإجراءات المرفوضة.

- إنشاء مكون StepProgress لعرض تقدم معالج متعدد الخطوات بشكل مرئي.

- تعريف أنواع مشتركة للإجراءات، وخطوات التدفق، ورسائل الدردشة في ملف types.ts.

- إضافة اختبارات وحدة لخطافات useFileUpload و useTaskPolling لضمان الأداء السليم ومعالجة الأخطاء.

- تنفيذ اختبارات واجهة برمجة التطبيقات (API) للتحقق من تنسيقات نقاط النهاية وضمان اتساق ربط الواجهة الأمامية بالخلفية.
This commit is contained in:
Your Name
2026-03-06 17:16:09 +02:00
parent 2e97741d60
commit cfbcc8bd79
62 changed files with 10567 additions and 101 deletions

View 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)$/);
});
});
});

View File

@@ -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;
}
/**