pos_bluetooth_thermal_printer/static/src/tests/escpos_properties.test.js
2025-11-21 05:52:53 +07:00

619 lines
23 KiB
JavaScript

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