mirror of
https://github.com/waaraawa/ByteGrid.git
synced 2026-07-22 06:41:47 +00:00
- Add autoColor option (default: true)
- true: Auto-assign colors to fields without explicit colors
- false: Use gray for fields without explicit colors
- Auto color sequence: blue, cyan, yellow, green, orange, purple, mint, pink
- Padding/reserved fields always use gray
- Explicit colors always take precedence
Examples & Documentation:
- Add 4 example files (basic, manual, mixed, interactive guide)
- Update examples/README.md with Auto Color section
- Add Color Options explanation
Tests:
- Add 8 new tests (5 layoutEngine, 3 parser)
- Update existing test for autoColor: false
508 lines
15 KiB
TypeScript
508 lines
15 KiB
TypeScript
/**
|
|
* LayoutEngine module tests
|
|
* Tests for converting Fields to LayoutBlocks
|
|
*/
|
|
|
|
import { createLayout } from '../src/layoutEngine';
|
|
import { ByteGridConfig } from '../src/types';
|
|
|
|
describe('LayoutEngine', () => {
|
|
describe('createLayout()', () => {
|
|
it('should create layout blocks for single-row fields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 16,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'blue' },
|
|
{ offset: '4-7', name: 'Field2', type: 'uint32_t', color: 'cyan' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
|
|
// Field1: bytes 0-3
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(4);
|
|
expect(blocks[0].fieldName).toBe('Field1');
|
|
expect(blocks[0].color).toBe('blue');
|
|
expect(blocks[0].offsetStart).toBe(0);
|
|
expect(blocks[0].offsetEnd).toBe(3);
|
|
|
|
// Field2: bytes 4-7
|
|
expect(blocks[1].row).toBe(0);
|
|
expect(blocks[1].col).toBe(4);
|
|
expect(blocks[1].span).toBe(4);
|
|
expect(blocks[1].fieldName).toBe('Field2');
|
|
expect(blocks[1].color).toBe('cyan');
|
|
});
|
|
|
|
it('should split field across multiple rows', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 32,
|
|
layout: 8,
|
|
fields: [
|
|
{ offset: '0-15', name: 'LargeField', type: 'uint8_t[16]', color: 'green' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
// Should split into 2 blocks (8 bytes each)
|
|
expect(blocks).toHaveLength(2);
|
|
|
|
// First row: bytes 0-7
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(8);
|
|
expect(blocks[0].fieldName).toBe('LargeField');
|
|
expect(blocks[0].offsetStart).toBe(0);
|
|
expect(blocks[0].offsetEnd).toBe(7);
|
|
|
|
// Second row: bytes 8-15
|
|
expect(blocks[1].row).toBe(1);
|
|
expect(blocks[1].col).toBe(0);
|
|
expect(blocks[1].span).toBe(8);
|
|
expect(blocks[1].fieldName).toBe('LargeField');
|
|
expect(blocks[1].offsetStart).toBe(8);
|
|
expect(blocks[1].offsetEnd).toBe(15);
|
|
});
|
|
|
|
it('should handle field starting mid-row', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 8,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t' },
|
|
{ offset: '4-11', name: 'Field2', type: 'uint64_t', color: 'purple' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
|
|
// Field1: bytes 0-3 (row 0, col 0)
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(4);
|
|
|
|
// Field2 part 1: bytes 4-7 (row 0, col 4)
|
|
expect(blocks[1].row).toBe(0);
|
|
expect(blocks[1].col).toBe(4);
|
|
expect(blocks[1].span).toBe(4);
|
|
expect(blocks[1].fieldName).toBe('Field2');
|
|
expect(blocks[1].offsetStart).toBe(4);
|
|
expect(blocks[1].offsetEnd).toBe(7);
|
|
|
|
// Field2 part 2: bytes 8-11 (row 1, col 0)
|
|
expect(blocks[2].row).toBe(1);
|
|
expect(blocks[2].col).toBe(0);
|
|
expect(blocks[2].span).toBe(4);
|
|
expect(blocks[2].fieldName).toBe('Field2');
|
|
expect(blocks[2].offsetStart).toBe(8);
|
|
expect(blocks[2].offsetEnd).toBe(11);
|
|
});
|
|
|
|
it('should default to gray color for fields without color when autoColor is false', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 8,
|
|
autoColor: false,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks[0].color).toBe('gray');
|
|
});
|
|
|
|
it('should mark reserved and padding types', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Data', type: 'uint32_t' },
|
|
{ offset: '4-7', name: 'Reserved', type: 'reserved' },
|
|
{ offset: '8-11', name: 'Padding', type: 'padding' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks[0].isPadding).toBe(false);
|
|
expect(blocks[1].isPadding).toBe(true);
|
|
expect(blocks[2].isPadding).toBe(true);
|
|
});
|
|
|
|
it('should preserve field metadata', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
fields: [
|
|
{
|
|
offset: '0-3',
|
|
name: 'ChunkID',
|
|
type: 'char[4]',
|
|
value: 'RIFF',
|
|
description: 'RIFF magic number',
|
|
color: 'blue',
|
|
},
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks[0].value).toBe('RIFF');
|
|
expect(blocks[0].description).toBe('RIFF magic number');
|
|
});
|
|
|
|
it('should handle single-byte fields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 8,
|
|
fields: [
|
|
{ offset: '0', name: 'Byte0', type: 'uint8_t' },
|
|
{ offset: '1', name: 'Byte1', type: 'uint8_t' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
expect(blocks[0].span).toBe(1);
|
|
expect(blocks[1].span).toBe(1);
|
|
});
|
|
|
|
it('should handle bitfields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 8,
|
|
fields: [
|
|
{
|
|
offset: '0',
|
|
name: 'Flags',
|
|
type: 'uint8_t',
|
|
bitfields: [
|
|
{ name: 'Bit7', bits: '7' },
|
|
{ name: 'Bit6', bits: '6' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks[0].bitfields).toBeDefined();
|
|
expect(blocks[0].bitfields).toHaveLength(2);
|
|
});
|
|
|
|
it('should work with layout=16 (default)', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 32,
|
|
fields: [
|
|
{ offset: '0-15', name: 'Row1', type: 'uint8_t[16]' },
|
|
{ offset: '16-31', name: 'Row2', type: 'uint8_t[16]' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[1].row).toBe(1);
|
|
});
|
|
|
|
it('should handle complex field arrangement', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'WAV Header',
|
|
size: 44,
|
|
layout: 16,
|
|
fields: [
|
|
{ offset: '0-3', name: 'ChunkID', type: 'char[4]', color: 'blue' },
|
|
{ offset: '4-7', name: 'ChunkSize', type: 'uint32_t', color: 'cyan' },
|
|
{ offset: '8-11', name: 'Format', type: 'char[4]', color: 'blue' },
|
|
{ offset: '12-15', name: 'Subchunk1ID', type: 'char[4]', color: 'orange' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
// All in first row
|
|
expect(blocks).toHaveLength(4);
|
|
blocks.forEach((block) => {
|
|
expect(block.row).toBe(0);
|
|
});
|
|
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[1].col).toBe(4);
|
|
expect(blocks[2].col).toBe(8);
|
|
expect(blocks[3].col).toBe(12);
|
|
});
|
|
});
|
|
|
|
describe('createLayout() - bit-unit layout', () => {
|
|
it('should create layout blocks for bit-unit fields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Bit Layout',
|
|
size: 16, // 16 bits
|
|
layoutUnit: 'bit',
|
|
layout: 16, // 16 bits per row
|
|
fields: [
|
|
{ offset: '0-7b', name: 'FirstByte', type: 'uint8_t', color: 'blue' },
|
|
{ offset: '8-15b', name: 'SecondByte', type: 'uint8_t', color: 'cyan' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
|
|
// FirstByte: bits 0-7
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(8);
|
|
expect(blocks[0].fieldName).toBe('FirstByte');
|
|
expect(blocks[0].offsetStart).toBe(0);
|
|
expect(blocks[0].offsetEnd).toBe(7);
|
|
|
|
// SecondByte: bits 8-15
|
|
expect(blocks[1].row).toBe(0);
|
|
expect(blocks[1].col).toBe(8);
|
|
expect(blocks[1].span).toBe(8);
|
|
expect(blocks[1].fieldName).toBe('SecondByte');
|
|
});
|
|
|
|
it('should handle Byte offset in bit layout', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Mixed',
|
|
size: 16, // 16 bits
|
|
layoutUnit: 'bit',
|
|
layout: 16,
|
|
fields: [
|
|
{ offset: '0B', name: 'ByteField', type: 'uint8_t', color: 'blue' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(1);
|
|
// 0B = byte 0 = bits 0-7
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(8);
|
|
expect(blocks[0].offsetStart).toBe(0);
|
|
expect(blocks[0].offsetEnd).toBe(7);
|
|
});
|
|
|
|
it('should split bit fields across multiple rows', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Multi-row Bits',
|
|
size: 32, // 32 bits
|
|
layoutUnit: 'bit',
|
|
layout: 16, // 16 bits per row
|
|
fields: [
|
|
{ offset: '0-23b', name: 'LargeBitField', type: 'bits', color: 'green' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
// Should split into 2 blocks (16 bits each)
|
|
expect(blocks).toHaveLength(2);
|
|
|
|
// First row: bits 0-15
|
|
expect(blocks[0].row).toBe(0);
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(16);
|
|
expect(blocks[0].offsetStart).toBe(0);
|
|
expect(blocks[0].offsetEnd).toBe(15);
|
|
|
|
// Second row: bits 16-23
|
|
expect(blocks[1].row).toBe(1);
|
|
expect(blocks[1].col).toBe(0);
|
|
expect(blocks[1].span).toBe(8);
|
|
expect(blocks[1].offsetStart).toBe(16);
|
|
expect(blocks[1].offsetEnd).toBe(23);
|
|
});
|
|
|
|
it('should handle mixed bit and Byte offsets', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'IPv4 Header',
|
|
size: 32, // 32 bits
|
|
layoutUnit: 'bit',
|
|
layout: 32,
|
|
fields: [
|
|
{ offset: '0-3b', name: 'Version', type: 'bits', color: 'blue' },
|
|
{ offset: '4-7b', name: 'IHL', type: 'bits', color: 'cyan' },
|
|
{ offset: '1B', name: 'DSCP', type: 'uint8_t', color: 'yellow' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
|
|
// Version: bits 0-3
|
|
expect(blocks[0].col).toBe(0);
|
|
expect(blocks[0].span).toBe(4);
|
|
|
|
// IHL: bits 4-7
|
|
expect(blocks[1].col).toBe(4);
|
|
expect(blocks[1].span).toBe(4);
|
|
|
|
// DSCP: byte 1 = bits 8-15
|
|
expect(blocks[2].col).toBe(8);
|
|
expect(blocks[2].span).toBe(8);
|
|
expect(blocks[2].offsetStart).toBe(8);
|
|
expect(blocks[2].offsetEnd).toBe(15);
|
|
});
|
|
|
|
it('should handle 4-bit fields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: '4-bit Fields',
|
|
size: 8, // 8 bits = 1 byte
|
|
layoutUnit: 'bit',
|
|
layout: 8,
|
|
fields: [
|
|
{ offset: '0-3b', name: 'Nibble1', type: 'bits' },
|
|
{ offset: '4-7b', name: 'Nibble2', type: 'bits' },
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
expect(blocks[0].span).toBe(4);
|
|
expect(blocks[1].span).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('autoColor feature', () => {
|
|
it('should auto-assign colors when autoColor is true (default)', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 16,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // No color specified
|
|
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // No color specified
|
|
{ offset: '8-11', name: 'Field3', type: 'uint32_t' }, // No color specified
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
// Should auto-assign: blue, cyan, yellow
|
|
expect(blocks[0].color).toBe('blue');
|
|
expect(blocks[1].color).toBe('cyan');
|
|
expect(blocks[2].color).toBe('yellow');
|
|
});
|
|
|
|
it('should use gray when autoColor is false and no color specified', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 16,
|
|
autoColor: false,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // No color specified
|
|
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // No color specified
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(2);
|
|
// Should default to gray
|
|
expect(blocks[0].color).toBe('gray');
|
|
expect(blocks[1].color).toBe('gray');
|
|
});
|
|
|
|
it('should respect explicit colors even when autoColor is true', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 16,
|
|
autoColor: true,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'pink' }, // Explicit color
|
|
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // Auto-assign
|
|
{ offset: '8-11', name: 'Field3', type: 'uint32_t', color: 'mint' }, // Explicit color
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
expect(blocks[0].color).toBe('pink'); // Explicit
|
|
expect(blocks[1].color).toBe('cyan'); // Auto (fieldIndex=1 -> cyan)
|
|
expect(blocks[2].color).toBe('mint'); // Explicit
|
|
});
|
|
|
|
it('should use gray for padding/reserved fields regardless of autoColor', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 16,
|
|
layout: 16,
|
|
autoColor: true,
|
|
fields: [
|
|
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // Auto-assign
|
|
{ offset: '4-7', name: 'Reserved', type: 'reserved' }, // Should be gray
|
|
{ offset: '8-11', name: 'Field2', type: 'uint32_t' }, // Auto-assign
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(3);
|
|
expect(blocks[0].color).toBe('blue'); // Auto
|
|
expect(blocks[1].color).toBe('gray'); // Reserved
|
|
expect(blocks[2].color).toBe('yellow'); // Auto (fieldIndex=2 -> yellow)
|
|
});
|
|
|
|
it('should cycle through colors for many fields', () => {
|
|
const config: ByteGridConfig = {
|
|
name: 'Test',
|
|
size: 36,
|
|
layout: 16,
|
|
autoColor: true,
|
|
fields: [
|
|
{ offset: '0-3', name: 'F1', type: 'uint32_t' },
|
|
{ offset: '4-7', name: 'F2', type: 'uint32_t' },
|
|
{ offset: '8-11', name: 'F3', type: 'uint32_t' },
|
|
{ offset: '12-15', name: 'F4', type: 'uint32_t' },
|
|
{ offset: '16-19', name: 'F5', type: 'uint32_t' },
|
|
{ offset: '20-23', name: 'F6', type: 'uint32_t' },
|
|
{ offset: '24-27', name: 'F7', type: 'uint32_t' },
|
|
{ offset: '28-31', name: 'F8', type: 'uint32_t' },
|
|
{ offset: '32-35', name: 'F9', type: 'uint32_t' }, // Should cycle back to blue
|
|
],
|
|
};
|
|
|
|
const blocks = createLayout(config);
|
|
|
|
expect(blocks).toHaveLength(9);
|
|
// First 8 colors
|
|
expect(blocks[0].color).toBe('blue');
|
|
expect(blocks[1].color).toBe('cyan');
|
|
expect(blocks[2].color).toBe('yellow');
|
|
expect(blocks[3].color).toBe('green');
|
|
expect(blocks[4].color).toBe('orange');
|
|
expect(blocks[5].color).toBe('purple');
|
|
expect(blocks[6].color).toBe('mint');
|
|
expect(blocks[7].color).toBe('pink');
|
|
// Cycle back
|
|
expect(blocks[8].color).toBe('blue');
|
|
});
|
|
});
|
|
});
|