diff --git a/LEARNING.md b/LEARNING.md index 1d4061c..cbbe2fe 100644 --- a/LEARNING.md +++ b/LEARNING.md @@ -161,11 +161,21 @@ The table system goes beyond simple formatting to provide hardware-aware renderi - **Font A Enforcement**: Consistent 12-dot character width for predictable layouts - **Paper Support**: 58mm (32 chars) and 80mm (48 chars) at 203 DPI +### 🧪 Quality Assurance & Testing + +- **Table-Driven Testing**: Implemented comprehensive table-driven unit tests for the `TableEngine` to verify rendering logic, ensuring support for: + - Header visibility toggling (explicit vs. data-driven). + - Column alignment (Left, Center, Right) with precise padding calculations. + - Word wrapping functionality for long text. + - Edge cases like nil data, invalid definitions, and empty rows. +- **Mocking & Buffering**: Utilized `bytes.Buffer` as an `io.Writer` to capture and inspect ESC/POS command output without physical hardware, enabling deterministic verification of control codes (e.g., bold toggling `ESC E`). + ```go // Example: Table that would overflow gets auto-reduced // Original: [20, 5, 12] = 39 chars (exceeds 32 max for 58mm) // After: [13, 5, 12] = 32 chars (fits perfectly) // Log: "Table auto-reduced: 39 → 32 chars (7 reductions applied)" +``` ## Module Dependencies diff --git a/pkg/tables/table_engine_test.go b/pkg/tables/table_engine_test.go new file mode 100644 index 0000000..d716fce --- /dev/null +++ b/pkg/tables/table_engine_test.go @@ -0,0 +1,178 @@ +package tables + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/adcondev/poster/pkg/constants" +) + +// TestRender tests the TabEngine.Render method +func TestRender(t *testing.T) { + // Common test data + def := Definition{ + Columns: []Column{ + {Name: "Item", Width: 10, Align: constants.Left}, + {Name: "Qty", Width: 5, Align: constants.Center}, + {Name: "Price", Width: 8, Align: constants.Right}, + }, + } + + tests := []struct { + name string + data *Data + opts *Options + expected []string // Expected substrings + unexpected []string // Unexpected substrings + expectError bool + errorMsg string + }{ + { + name: "Render Basic Table with Headers", + data: &Data{ + Definition: def, + ShowHeaders: true, + Rows: []Row{ + {"Apple", "10", "1.50"}, + }, + }, + opts: DefaultOptions(), + expected: []string{ + "\x1bE\x01", // Enable Bold + "Item Qty Price", // Header row + "\x1bE\x00", // Disable Bold + "Apple 10 1.50", // Data row + }, + expectError: false, + }, + { + name: "Render Table without Headers", + data: &Data{ + Definition: def, + ShowHeaders: false, + Rows: []Row{ + {"Banana", "5", "0.99"}, + }, + }, + opts: &Options{ + PaperWidth: 80, + ShowHeaders: false, + HeaderStyle: Style{Bold: true}, + WordWrap: true, + ColumnSpacing: 1, + }, + expected: []string{ + "Banana 5 0.99", // Data row + }, + unexpected: []string{ + "Item", "Qty", "Price", // Headers should not be present + "\x1bE\x01", // No bold command + }, + expectError: false, + }, + { + name: "Render Table with Alignment", + data: &Data{ + Definition: def, + Rows: []Row{ + {"Left", "Cnt", "Right"}, + }, + }, + opts: &Options{ + PaperWidth: 80, + ShowHeaders: false, + HeaderStyle: Style{Bold: true}, + WordWrap: true, + ColumnSpacing: 1, + }, + expected: []string{ + "Left Cnt Right", + }, + expectError: false, + }, + { + name: "Render Table with Word Wrap", + data: &Data{ + Definition: Definition{ + Columns: []Column{ + {Name: "Desc", Width: 5, Align: constants.Left}, + }, + }, + Rows: []Row{ + {"Long Text"}, // Should wrap to "Long " and "Text " + }, + }, + opts: DefaultOptions(), + expected: []string{ + "Long ", + "Text ", + }, + expectError: false, + }, + { + name: "Error: Nil Data", + data: nil, + opts: DefaultOptions(), + expectError: true, + errorMsg: "table data cannot be nil", + }, + { + name: "Error: Invalid Data (Row Length Mismatch)", + data: &Data{ + Definition: def, + Rows: []Row{ + {"Apple", "10"}, // Missing one column + }, + }, + opts: DefaultOptions(), + expectError: true, + errorMsg: "invalid table data", + }, + { + name: "Error: Invalid Data (Empty definition)", + data: &Data{ + Definition: Definition{}, + Rows: []Row{ + {"Apple"}, + }, + }, + opts: DefaultOptions(), + expectError: true, + errorMsg: "table must have at least one column", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := NewEngine(&def, tt.opts) + var buf bytes.Buffer + + err := engine.Render(&buf, tt.data) + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + output := buf.String() + + for _, exp := range tt.expected { + assert.Contains(t, output, exp, "Output should contain expected string") + } + + for _, unexp := range tt.unexpected { + assert.NotContains(t, output, unexp, "Output should NOT contain unexpected string") + } + + // Verify Line Feeds are present + if len(tt.expected) > 0 { + assert.True(t, strings.HasSuffix(output, "\n"), "Output should end with a newline") + } + } + }) + } +}