/** * Property-Based Tests for ESC/POS Command Generation * * Tests correctness properties related to ESC/POS command generation * using fast-check for property-based testing. */ import * as fc from 'fast-check'; import { EscPosGenerator } from '../js/escpos_generator.js'; describe('ESC/POS Conversion Properties', () => { let generator; beforeEach(() => { generator = new EscPosGenerator(); }); /** * Generator for receipt data structures * Generates valid receipt objects with random data */ const receiptDataGenerator = () => { return fc.record({ headerData: fc.record({ companyName: fc.string({ minLength: 1, maxLength: 100 }), address: fc.string({ minLength: 1, maxLength: 200 }), phone: fc.string({ minLength: 1, maxLength: 20 }), taxId: fc.string({ minLength: 1, maxLength: 50 }) }), orderData: fc.record({ orderName: fc.string({ minLength: 1, maxLength: 50 }), date: fc.date().map(d => d.toISOString()), cashier: fc.string({ minLength: 1, maxLength: 50 }), customer: fc.option(fc.string({ minLength: 1, maxLength: 100 })) }), lines: fc.array( fc.record({ productName: fc.string({ minLength: 1, maxLength: 100 }), quantity: fc.float({ min: Math.fround(0.01), max: Math.fround(1000), noNaN: true }), price: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }), total: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }) }), { minLength: 1, maxLength: 50 } ), totals: fc.record({ subtotal: fc.float({ min: Math.fround(0), max: Math.fround(1000000), noNaN: true }), tax: fc.float({ min: Math.fround(0), max: Math.fround(100000), noNaN: true }), discount: fc.float({ min: Math.fround(0), max: Math.fround(100000), noNaN: true }), total: fc.float({ min: Math.fround(0), max: Math.fround(1000000), noNaN: true }) }), paymentData: fc.record({ method: fc.constantFrom('cash', 'card', 'mobile'), amount: fc.float({ min: Math.fround(0), max: Math.fround(1000000), noNaN: true }), change: fc.float({ min: Math.fround(0), max: Math.fround(100000), noNaN: true }) }), footerData: fc.record({ message: fc.string({ maxLength: 200 }), barcode: fc.option(fc.string({ minLength: 1, maxLength: 50 })) }) }); }; /** * Generator for alignment values */ const alignmentGenerator = () => { return fc.constantFrom('left', 'center', 'right'); }; /** * Generator for text size values (1-8) */ const textSizeGenerator = () => { return fc.integer({ min: 1, max: 8 }); }; /** * Generator for text strings */ const textGenerator = () => { return fc.string({ minLength: 0, maxLength: 200 }); }; /** * Helper function to check if a Uint8Array contains a specific byte sequence */ const containsSequence = (array, sequence) => { for (let i = 0; i <= array.length - sequence.length; i++) { let found = true; for (let j = 0; j < sequence.length; j++) { if (array[i + j] !== sequence[j]) { found = false; break; } } if (found) return true; } return false; }; /** * Feature: pos-bluetooth-thermal-printer, Property 7: ESC/POS conversion correctness * * Property: For any receipt data, the ESC/POS generator should produce valid * command sequences that include all formatting directives (alignment, size, * emphasis, line feeds, cuts) * * Validates: Requirements 3.2, 7.1, 7.2, 7.3, 7.4 */ test('Property 7: ESC/POS conversion correctness - generates valid command sequences', () => { fc.assert( fc.property( receiptDataGenerator(), (receiptData) => { // Generate ESC/POS commands const result = generator.generateReceipt(receiptData); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify result is not empty expect(result.length).toBeGreaterThan(0); // Verify initialization command is present (ESC @ = 0x1B 0x40) expect(containsSequence(result, [0x1B, 0x40])).toBe(true); // Verify paper cut command is present (GS V 0 = 0x1D 0x56 0x00) expect(containsSequence(result, [0x1D, 0x56, 0x00])).toBe(true); // Verify line feed commands are present (LF = 0x0A) expect(result.includes(0x0A)).toBe(true); return true; } ), { numRuns: 100 } ); }); /** * Property: Alignment commands are correctly generated * * Tests that setAlignment produces the correct ESC/POS command for each alignment type * * Validates: Requirements 7.1 */ test('Property 7a: Alignment commands are correctly generated', () => { fc.assert( fc.property( alignmentGenerator(), (alignment) => { const result = generator.setAlignment(alignment); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify it starts with ESC 'a' (0x1B 0x61) expect(result[0]).toBe(0x1B); expect(result[1]).toBe(0x61); // Verify correct alignment value if (alignment === 'left') { expect(result[2]).toBe(0x00); } else if (alignment === 'center') { expect(result[2]).toBe(0x01); } else if (alignment === 'right') { expect(result[2]).toBe(0x02); } return true; } ), { numRuns: 100 } ); }); /** * Property: Text size commands are correctly generated * * Tests that setTextSize produces valid ESC/POS commands for all size combinations * * Validates: Requirements 7.2 */ test('Property 7b: Text size commands are correctly generated', () => { fc.assert( fc.property( textSizeGenerator(), textSizeGenerator(), (width, height) => { const result = generator.setTextSize(width, height); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify it starts with GS '!' (0x1D 0x21) expect(result[0]).toBe(0x1D); expect(result[1]).toBe(0x21); // Verify size value is within valid range (0x00 to 0x77) expect(result[2]).toBeGreaterThanOrEqual(0x00); expect(result[2]).toBeLessThanOrEqual(0x77); // Verify the size encoding is correct const widthValue = width - 1; const heightValue = height - 1; const expectedSize = (widthValue << 4) | heightValue; expect(result[2]).toBe(expectedSize); return true; } ), { numRuns: 100 } ); }); /** * Property: Emphasis commands are correctly generated * * Tests that setEmphasis produces valid ESC/POS commands for bold and underline * * Validates: Requirements 7.3 */ test('Property 7c: Emphasis commands are correctly generated', () => { fc.assert( fc.property( fc.boolean(), fc.boolean(), (bold, underline) => { const result = generator.setEmphasis(bold, underline); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify bold command is present if (bold) { expect(containsSequence(result, [0x1B, 0x45, 0x01])).toBe(true); } else { expect(containsSequence(result, [0x1B, 0x45, 0x00])).toBe(true); } // Verify underline command is present if (underline) { expect(containsSequence(result, [0x1B, 0x2D, 0x01])).toBe(true); } else { expect(containsSequence(result, [0x1B, 0x2D, 0x00])).toBe(true); } return true; } ), { numRuns: 100 } ); }); /** * Property: Feed and cut commands are correctly generated * * Tests that feedAndCut produces valid ESC/POS commands with line feeds and paper cut * * Validates: Requirements 7.4 */ test('Property 7d: Feed and cut commands are correctly generated', () => { fc.assert( fc.property( fc.integer({ min: 0, max: 10 }), (lines) => { const result = generator.feedAndCut(lines); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify paper cut command is present (GS V 0 = 0x1D 0x56 0x00) expect(containsSequence(result, [0x1D, 0x56, 0x00])).toBe(true); // Count line feed commands (0x0A) let lfCount = 0; for (let i = 0; i < result.length; i++) { if (result[i] === 0x0A) { lfCount++; } } // Verify correct number of line feeds expect(lfCount).toBe(lines); return true; } ), { numRuns: 100 } ); }); /** * Property: Text encoding produces non-empty output for non-empty input * * Tests that encodeText correctly handles text encoding * * Validates: Requirements 3.2 */ test('Property 7e: Text encoding produces output for non-empty input', () => { fc.assert( fc.property( fc.string({ minLength: 1, maxLength: 200 }), (text) => { const result = generator.encodeText(text); // Verify result is a typed array (use duck typing to avoid constructor issues) expect(result).toBeDefined(); expect(typeof result.length).toBe('number'); expect(typeof result.buffer).toBe('object'); // Verify non-empty text produces non-empty output expect(result.length).toBeGreaterThan(0); return true; } ), { numRuns: 100 } ); }); /** * Property: Empty text encoding produces empty output * * Tests that encodeText correctly handles empty strings * * Validates: Requirements 3.2 */ test('Property 7f: Empty text encoding produces empty output', () => { const result = generator.encodeText(''); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBe(0); }); /** * Property: Receipt generation includes all major sections * * Tests that generateReceipt includes commands for all provided data sections * * Validates: Requirements 3.2, 7.1, 7.2, 7.3, 7.4 */ test('Property 7g: Receipt generation includes all major sections', () => { fc.assert( fc.property( receiptDataGenerator(), (receiptData) => { const result = generator.generateReceipt(receiptData); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify alignment commands are present (at least one) const hasAlignment = containsSequence(result, [0x1B, 0x61, 0x00]) || containsSequence(result, [0x1B, 0x61, 0x01]) || containsSequence(result, [0x1B, 0x61, 0x02]); expect(hasAlignment).toBe(true); // Verify text size commands are present (GS !) expect(containsSequence(result, [0x1D, 0x21])).toBe(true); // Verify emphasis commands are present (ESC E) expect(containsSequence(result, [0x1B, 0x45])).toBe(true); // Verify line feeds are present expect(result.includes(0x0A)).toBe(true); // Verify paper cut is present expect(containsSequence(result, [0x1D, 0x56, 0x00])).toBe(true); return true; } ), { numRuns: 100 } ); }); /** * Property: Initialize command is correctly generated * * Tests that initialize produces the correct ESC/POS initialization command * * Validates: Requirements 3.2 */ test('Property 7h: Initialize command is correctly generated', () => { const result = generator.initialize(); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBe(2); expect(result[0]).toBe(0x1B); expect(result[1]).toBe(0x40); }); /** * Property: AddLine produces valid command sequence * * Tests that addLine generates proper ESC/POS commands with formatting * * Validates: Requirements 3.2, 7.1, 7.2, 7.3 */ test('Property 7i: AddLine produces valid command sequence', () => { fc.assert( fc.property( textGenerator(), fc.record({ align: fc.option(alignmentGenerator()), width: fc.option(textSizeGenerator()), height: fc.option(textSizeGenerator()), bold: fc.option(fc.boolean()), underline: fc.option(fc.boolean()) }), (text, options) => { const result = generator.addLine(text, options); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify line feed is present expect(result.includes(0x0A)).toBe(true); // If alignment specified, verify alignment command if (options.align) { expect(containsSequence(result, [0x1B, 0x61])).toBe(true); } // If size specified, verify size command if (options.width || options.height) { expect(containsSequence(result, [0x1D, 0x21])).toBe(true); } // If emphasis specified, verify emphasis commands if (options.bold !== undefined || options.underline !== undefined) { expect(containsSequence(result, [0x1B, 0x45])).toBe(true); } return true; } ), { numRuns: 100 } ); }); /** * Feature: pos-bluetooth-thermal-printer, Property 13: Character encoding correctness * * Property: For any text in the receipt, the ESC/POS generator should encode it * using the configured character set for the printer * * Validates: Requirements 7.5 */ test('Property 13: Character encoding correctness', () => { /** * Generator for character sets supported by ESC/POS printers */ const characterSetGenerator = () => { return fc.constantFrom('CP437', 'CP850', 'CP852', 'CP858', 'UTF-8'); }; /** * Generator for text with various character types * Includes ASCII, extended ASCII, and special characters */ const mixedTextGenerator = () => { return fc.oneof( // Basic ASCII text fc.string({ minLength: 1, maxLength: 100 }), // Text with numbers and symbols fc.string({ minLength: 1, maxLength: 100 }).map(s => s + ' $123.45'), // Text with common receipt characters fc.constantFrom( 'Total: $99.99', 'Qty: 5 @ $10.00', 'Tax (15%): $15.00', 'Receipt #12345', '*** THANK YOU ***', '================================', 'Item Price' ), // Text with extended ASCII characters (common in European languages) fc.constantFrom( 'Café au lait', 'Crème brûlée', 'Jalapeño', 'Naïve', 'Résumé', 'Über', 'Piñata' ) ); }; fc.assert( fc.property( characterSetGenerator(), mixedTextGenerator(), (charset, text) => { // Configure the generator with the character set generator.characterSet = charset; // Encode the text const result = generator.encodeText(text); // Verify result is a typed array (use duck typing to avoid constructor issues) expect(result).toBeDefined(); expect(typeof result.length).toBe('number'); expect(typeof result.buffer).toBe('object'); // Verify non-empty text produces non-empty output if (text.length > 0) { expect(result.length).toBeGreaterThan(0); } // Verify all bytes are valid (0-255) for (let i = 0; i < result.length; i++) { expect(result[i]).toBeGreaterThanOrEqual(0); expect(result[i]).toBeLessThanOrEqual(255); } // Verify encoding is deterministic (same input produces same output) const result2 = generator.encodeText(text); expect(result.length).toBe(result2.length); for (let i = 0; i < result.length; i++) { expect(result[i]).toBe(result2[i]); } // Verify the encoding respects the character set configuration // The characterSet property should be used by the encoder expect(generator.characterSet).toBe(charset); return true; } ), { numRuns: 100 } ); }); /** * Property: Character encoding handles empty strings correctly * * Tests that encodeText returns empty array for empty input * * Validates: Requirements 7.5 */ test('Property 13a: Character encoding handles empty strings', () => { const charsets = ['CP437', 'CP850', 'CP852', 'CP858', 'UTF-8']; charsets.forEach(charset => { generator.characterSet = charset; const result = generator.encodeText(''); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBe(0); }); }); /** * Property: Character encoding handles null/undefined input * * Tests that encodeText gracefully handles invalid input * * Validates: Requirements 7.5 */ test('Property 13b: Character encoding handles null/undefined input', () => { const charsets = ['CP437', 'CP850', 'CP852', 'CP858', 'UTF-8']; charsets.forEach(charset => { generator.characterSet = charset; const resultNull = generator.encodeText(null); expect(resultNull).toBeInstanceOf(Uint8Array); expect(resultNull.length).toBe(0); const resultUndefined = generator.encodeText(undefined); expect(resultUndefined).toBeInstanceOf(Uint8Array); expect(resultUndefined.length).toBe(0); }); }); /** * Property: Character encoding is consistent across receipt generation * * Tests that the configured character set is used consistently when generating * a complete receipt * * Validates: Requirements 7.5 */ test('Property 13c: Character encoding is consistent in receipt generation', () => { fc.assert( fc.property( fc.constantFrom('CP437', 'CP850', 'CP852', 'CP858'), receiptDataGenerator(), (charset, receiptData) => { // Configure the generator with the character set generator.characterSet = charset; // Generate the receipt const result = generator.generateReceipt(receiptData); // Verify result is a Uint8Array expect(result).toBeInstanceOf(Uint8Array); // Verify all bytes are valid for (let i = 0; i < result.length; i++) { expect(result[i]).toBeGreaterThanOrEqual(0); expect(result[i]).toBeLessThanOrEqual(255); } // Verify the character set configuration is preserved expect(generator.characterSet).toBe(charset); return true; } ), { numRuns: 100 } ); }); });