const PrintableLayoutService = require('../services/PrintableLayoutService'); // Mock the CodeGenerationService jest.mock('../services/CodeGenerationService', () => { return jest.fn().mockImplementation(() => ({ generateBarcode: jest.fn().mockResolvedValue({ success: true, data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', format: 'CODE128', productCode: 'TEST123' }), generateQRCode: jest.fn().mockResolvedValue({ success: true, data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', productCode: 'TEST123', embeddedData: { code: 'TEST123', desc: 'Test Product' } }) })); }); describe('PrintableLayoutService', () => { let layoutService; let sampleProducts; beforeEach(() => { layoutService = new PrintableLayoutService(); sampleProducts = [ { product_code: 'PROD001', description: 'Test Product 1', category: 'Electronics', unit_of_measure: 'pcs' }, { product_code: 'PROD002', description: 'Test Product 2', category: 'Hardware', unit_of_measure: 'pcs' }, { product_code: 'PROD003', description: 'Test Product 3 with a very long description that should be truncated', category: 'Software', unit_of_measure: 'licenses' } ]; }); describe('Constructor and Configuration', () => { test('should initialize with default options', () => { expect(layoutService.labelSizes).toHaveProperty('avery-5160'); expect(layoutService.labelSizes).toHaveProperty('avery-5161'); expect(layoutService.labelSizes).toHaveProperty('custom'); expect(layoutService.defaultLayoutOptions.labelSize).toBe('avery-5160'); expect(layoutService.defaultLayoutOptions.includeBarcode).toBe(true); expect(layoutService.defaultLayoutOptions.includeQRCode).toBe(false); }); test('should return available label sizes', () => { const sizes = layoutService.getAvailableLabelSizes(); expect(sizes).toHaveProperty('avery-5160'); expect(sizes).toHaveProperty('avery-5161'); expect(sizes).toHaveProperty('custom'); expect(sizes['avery-5160']).toHaveProperty('width'); expect(sizes['avery-5160']).toHaveProperty('height'); expect(sizes['avery-5160']).toHaveProperty('columns'); expect(sizes['avery-5160']).toHaveProperty('rows'); }); }); describe('Layout Preview Generation', () => { test('should generate layout preview with default options', async () => { const result = await layoutService.generateLayoutPreview(sampleProducts); expect(result.success).toBe(true); expect(result.preview).toHaveProperty('labelSize', 'avery-5160'); expect(result.preview).toHaveProperty('dimensions'); expect(result.preview).toHaveProperty('labelsPerPage'); expect(result.preview).toHaveProperty('totalPages'); expect(result.preview.totalPages).toBe(1); // 3 products, 30 labels per page expect(result.preview.includeBarcode).toBe(true); expect(result.preview.includeQRCode).toBe(false); }); test('should generate layout preview with custom options', async () => { const options = { labelSize: 'avery-5161', includeQRCode: true, includeBarcode: false }; const result = await layoutService.generateLayoutPreview(sampleProducts, options); expect(result.success).toBe(true); expect(result.preview.labelSize).toBe('avery-5161'); expect(result.preview.includeBarcode).toBe(false); expect(result.preview.includeQRCode).toBe(true); }); test('should calculate correct number of pages', async () => { // Create more products to test pagination const manyProducts = Array.from({ length: 35 }, (_, i) => ({ product_code: `PROD${String(i + 1).padStart(3, '0')}`, description: `Product ${i + 1}`, category: 'Test', unit_of_measure: 'pcs' })); const result = await layoutService.generateLayoutPreview(manyProducts); expect(result.success).toBe(true); // avery-5160 has 30 labels per page (3 columns × 10 rows) // 35 products should require 2 pages expect(result.preview.totalPages).toBe(2); }); test('should handle empty products array', async () => { const result = await layoutService.generateLayoutPreview([]); expect(result.success).toBe(true); expect(result.preview.totalPages).toBe(0); }); }); describe('Custom Template Generation', () => { test('should generate custom template with valid dimensions', async () => { const templateOptions = { width: 40, height: 20, columns: 4, rows: 10, name: 'my-custom-template' }; const result = await layoutService.generateCustomTemplate(templateOptions); expect(result.success).toBe(true); expect(result.template.name).toBe('my-custom-template'); expect(result.template.width).toBe(40); expect(result.template.height).toBe(20); expect(result.template.columns).toBe(4); expect(result.template.rows).toBe(10); expect(result.template.totalLabels).toBe(40); expect(result.validation.fitsOnPage).toBe(true); }); test('should reject template that exceeds page width', async () => { const templateOptions = { width: 100, height: 20, columns: 5, rows: 5 }; const result = await layoutService.generateCustomTemplate(templateOptions); expect(result.success).toBe(false); expect(result.error).toContain('Template width exceeds page width'); }); test('should reject template that exceeds page height', async () => { const templateOptions = { width: 30, height: 50, columns: 2, rows: 10 }; const result = await layoutService.generateCustomTemplate(templateOptions); expect(result.success).toBe(false); expect(result.error).toContain('Template height exceeds page height'); }); test('should use default values for missing template options', async () => { const result = await layoutService.generateCustomTemplate({}); expect(result.success).toBe(true); expect(result.template.width).toBe(50); expect(result.template.height).toBe(25); expect(result.template.columns).toBe(4); expect(result.template.rows).toBe(8); expect(result.template.name).toBe('custom-template'); }); }); describe('PDF Layout Generation', () => { test('should generate PDF with barcode layout', async () => { const options = { labelSize: 'avery-5160', includeBarcode: true, includeQRCode: false, includeProductCode: true, includeDescription: true }; const result = await layoutService.generatePrintableLayout(sampleProducts, options); expect(result.success).toBe(true); expect(result.data).toBeInstanceOf(Buffer); expect(result.metadata.totalProducts).toBe(3); expect(result.metadata.totalPages).toBe(1); expect(result.metadata.labelSize).toBe('avery-5160'); expect(result.metadata.format).toBe('PDF'); }); test('should generate PDF with QR code layout', async () => { const options = { labelSize: 'avery-5161', includeBarcode: false, includeQRCode: true, includeProductCode: true, includeDescription: false }; const result = await layoutService.generatePrintableLayout(sampleProducts, options); expect(result.success).toBe(true); expect(result.data).toBeInstanceOf(Buffer); expect(result.metadata.labelSize).toBe('avery-5161'); }); test('should generate PDF with both barcode and QR code', async () => { const options = { includeBarcode: true, includeQRCode: true, includeProductCode: true, includeDescription: true }; const result = await layoutService.generatePrintableLayout(sampleProducts, options); expect(result.success).toBe(true); expect(result.data).toBeInstanceOf(Buffer); }); test('should handle empty products array', async () => { const result = await layoutService.generatePrintableLayout([]); expect(result.success).toBe(false); expect(result.error).toContain('Products array is required and cannot be empty'); }); test('should handle null products parameter', async () => { const result = await layoutService.generatePrintableLayout(null); expect(result.success).toBe(false); expect(result.error).toContain('Products array is required and cannot be empty'); }); test('should generate multiple pages for many products', async () => { // Create enough products to span multiple pages const manyProducts = Array.from({ length: 35 }, (_, i) => ({ product_code: `PROD${String(i + 1).padStart(3, '0')}`, description: `Product ${i + 1}`, category: 'Test', unit_of_measure: 'pcs' })); const result = await layoutService.generatePrintableLayout(manyProducts); expect(result.success).toBe(true); expect(result.metadata.totalProducts).toBe(35); expect(result.metadata.totalPages).toBe(2); }); test('should use custom label size', async () => { const options = { labelSize: 'custom', includeBarcode: true }; const result = await layoutService.generatePrintableLayout(sampleProducts, options); expect(result.success).toBe(true); expect(result.metadata.labelSize).toBe('custom'); }); test('should handle code generation failures gracefully', async () => { // Mock code generation service to return failure layoutService.codeGenService.generateBarcode.mockResolvedValueOnce({ success: false, error: 'Barcode generation failed' }); const result = await layoutService.generatePrintableLayout(sampleProducts); // Should still succeed but without the failed barcode expect(result.success).toBe(true); }); }); describe('Configuration Export/Import', () => { test('should export layout configuration', () => { const layoutConfig = { labelSize: 'avery-5160', includeBarcode: true, includeQRCode: false, fontSize: 10 }; const exported = layoutService.exportLayoutConfiguration(layoutConfig); expect(exported).toHaveProperty('version', '1.0'); expect(exported).toHaveProperty('timestamp'); expect(exported.configuration).toEqual({ ...layoutConfig, availableSizes: Object.keys(layoutService.labelSizes) }); }); test('should import valid layout configuration', () => { const configData = { version: '1.0', timestamp: '2023-01-01T00:00:00.000Z', configuration: { labelSize: 'avery-5161', includeBarcode: true, includeQRCode: true, fontSize: 12 } }; const result = layoutService.importLayoutConfiguration(configData); expect(result.success).toBe(true); expect(result.configuration.labelSize).toBe('avery-5161'); expect(result.message).toBe('Configuration imported successfully'); }); test('should reject invalid configuration format', () => { const invalidConfig = { version: '1.0', // missing configuration property }; const result = layoutService.importLayoutConfiguration(invalidConfig); expect(result.success).toBe(false); expect(result.error).toBe('Invalid configuration format'); }); test('should reject configuration with missing required fields', () => { const configData = { configuration: { includeBarcode: true // missing labelSize } }; const result = layoutService.importLayoutConfiguration(configData); expect(result.success).toBe(false); expect(result.error).toContain('Missing required field: labelSize'); }); test('should reject configuration with unsupported label size', () => { const configData = { configuration: { labelSize: 'unsupported-size', includeBarcode: true } }; const result = layoutService.importLayoutConfiguration(configData); expect(result.success).toBe(false); expect(result.error).toContain('Unsupported label size: unsupported-size'); }); }); describe('Text Truncation', () => { test('should truncate long text to fit width', () => { // Create a mock PDF object for text measurement const mockPdf = { getTextWidth: jest.fn((text) => text.length * 2) // Simple mock: 2mm per character }; const longText = 'This is a very long product description that should be truncated'; const maxWidth = 50; // 25 characters max at 2mm per character const truncated = layoutService._truncateText(longText, maxWidth, mockPdf); expect(truncated).toContain('...'); expect(mockPdf.getTextWidth(truncated)).toBeLessThanOrEqual(maxWidth); }); test('should return original text if it fits', () => { const mockPdf = { getTextWidth: jest.fn((text) => text.length * 2) }; const shortText = 'Short text'; const maxWidth = 50; const result = layoutService._truncateText(shortText, maxWidth, mockPdf); expect(result).toBe(shortText); }); test('should handle empty text', () => { const mockPdf = { getTextWidth: jest.fn(() => 0) }; const result = layoutService._truncateText('', 50, mockPdf); expect(result).toBe(''); }); test('should handle null text', () => { const mockPdf = { getTextWidth: jest.fn(() => 0) }; const result = layoutService._truncateText(null, 50, mockPdf); expect(result).toBe(''); }); }); describe('Private Methods', () => { test('should generate codes for products', async () => { const options = { includeBarcode: true, includeQRCode: true, barcodeFormat: 'CODE128' }; const codes = await layoutService._generateCodesForProducts(sampleProducts, options); expect(codes).toHaveLength(3); expect(codes[0]).toHaveProperty('product'); expect(codes[0]).toHaveProperty('barcode'); expect(codes[0]).toHaveProperty('qrCode'); expect(codes[0].barcode.success).toBe(true); expect(codes[0].qrCode.success).toBe(true); }); test('should generate codes with selective options', async () => { const options = { includeBarcode: true, includeQRCode: false }; const codes = await layoutService._generateCodesForProducts(sampleProducts, options); expect(codes[0].barcode).toBeTruthy(); expect(codes[0].qrCode).toBeNull(); }); }); });