pyWebLayout/tests/concrete/test_table_rendering.py

653 lines
20 KiB
Python

"""
Tests for table rendering components.
This module tests:
- TableStyle: Styling configuration for tables
- TableCellRenderer: Individual cell rendering
- TableRowRenderer: Row rendering with multiple cells
- TableRenderer: Complete table rendering
"""
import pytest
from PIL import Image, ImageDraw
from pyWebLayout.concrete.table import (
TableStyle,
TableCellRenderer,
TableRowRenderer,
TableRenderer
)
from pyWebLayout.abstract.block import (
Table, TableRow, TableCell, Paragraph, Heading, HeadingLevel
)
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def sample_font():
"""Create a standard font for testing."""
return Font(font_size=12, colour=(0, 0, 0))
@pytest.fixture
def sample_canvas():
"""Create a PIL canvas for rendering."""
return Image.new('RGB', (800, 600), color=(255, 255, 255))
@pytest.fixture
def sample_draw(sample_canvas):
"""Create a PIL ImageDraw object."""
return ImageDraw.Draw(sample_canvas)
@pytest.fixture
def default_table_style():
"""Create default table style."""
return TableStyle()
@pytest.fixture
def custom_table_style():
"""Create custom table style."""
return TableStyle(
border_width=2,
border_color=(100, 100, 100),
cell_padding=(10, 10, 10, 10),
header_bg_color=(200, 200, 200),
cell_bg_color=(250, 250, 250),
alternate_row_color=(240, 240, 240),
cell_spacing=5
)
@pytest.fixture
def simple_table(sample_font):
"""Create a simple table with header and body."""
table = Table()
table.caption = "Test Table"
# Header row
header_row = TableRow()
header_cell1 = TableCell(is_header=True)
header_p1 = Paragraph(sample_font)
header_p1.add_word(Word("Column", sample_font))
header_p1.add_word(Word("1", sample_font))
header_cell1.add_block(header_p1)
header_cell2 = TableCell(is_header=True)
header_p2 = Paragraph(sample_font)
header_p2.add_word(Word("Column", sample_font))
header_p2.add_word(Word("2", sample_font))
header_cell2.add_block(header_p2)
header_row.add_cell(header_cell1)
header_row.add_cell(header_cell2)
table.add_row(header_row, section="header")
# Body row
body_row = TableRow()
body_cell1 = TableCell()
body_p1 = Paragraph(sample_font)
body_p1.add_word(Word("Data", sample_font))
body_p1.add_word(Word("1", sample_font))
body_cell1.add_block(body_p1)
body_cell2 = TableCell()
body_p2 = Paragraph(sample_font)
body_p2.add_word(Word("Data", sample_font))
body_p2.add_word(Word("2", sample_font))
body_cell2.add_block(body_p2)
body_row.add_cell(body_cell1)
body_row.add_cell(body_cell2)
table.add_row(body_row, section="body")
return table
# ============================================================================
# TableStyle Tests
# ============================================================================
class TestTableStyle:
"""Tests for TableStyle dataclass."""
def test_default_initialization(self):
"""Test TableStyle with default values."""
style = TableStyle()
assert style.border_width == 1
assert style.border_color == (0, 0, 0)
assert style.cell_padding == (5, 5, 5, 5)
assert style.header_bg_color == (240, 240, 240)
assert style.header_text_bold is True
assert style.cell_bg_color == (255, 255, 255)
assert style.alternate_row_color == (250, 250, 250)
assert style.cell_spacing == 0
def test_custom_initialization(self):
"""Test TableStyle with custom values."""
style = TableStyle(
border_width=3,
border_color=(255, 0, 0),
cell_padding=(10, 15, 20, 25),
header_bg_color=(100, 100, 100),
header_text_bold=False,
cell_bg_color=(200, 200, 200),
alternate_row_color=None,
cell_spacing=10
)
assert style.border_width == 3
assert style.border_color == (255, 0, 0)
assert style.cell_padding == (10, 15, 20, 25)
assert style.header_bg_color == (100, 100, 100)
assert style.header_text_bold is False
assert style.cell_bg_color == (200, 200, 200)
assert style.alternate_row_color is None
assert style.cell_spacing == 10
def test_all_attributes_accessible(self, custom_table_style):
"""Test that all style attributes are accessible."""
# Verify all attributes exist and are accessible
assert hasattr(custom_table_style, 'border_width')
assert hasattr(custom_table_style, 'border_color')
assert hasattr(custom_table_style, 'cell_padding')
assert hasattr(custom_table_style, 'header_bg_color')
assert hasattr(custom_table_style, 'header_text_bold')
assert hasattr(custom_table_style, 'cell_bg_color')
assert hasattr(custom_table_style, 'alternate_row_color')
assert hasattr(custom_table_style, 'cell_spacing')
# ============================================================================
# TableCellRenderer Tests
# ============================================================================
class TestTableCellRenderer:
"""Tests for TableCellRenderer."""
def test_initialization(self, sample_font, sample_draw, default_table_style):
"""Test TableCellRenderer initialization."""
cell = TableCell()
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(100, 50),
draw=sample_draw,
style=default_table_style
)
assert cell_renderer._cell == cell
# Origin and size may be numpy arrays, so compare values
import numpy as np
assert np.array_equal(cell_renderer._origin, (10, 10))
assert np.array_equal(cell_renderer._size, (100, 50))
assert cell_renderer._draw == sample_draw
assert cell_renderer._style == default_table_style
assert cell_renderer._is_header_section is False
def test_initialization_with_header(
self,
sample_font,
sample_draw,
default_table_style):
"""Test TableCellRenderer initialization for header cell."""
cell = TableCell(is_header=True)
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(100, 50),
draw=sample_draw,
style=default_table_style,
is_header_section=True
)
assert cell_renderer._is_header_section is True
def test_render_empty_cell(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering an empty cell."""
cell = TableCell()
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(100, 50),
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = cell_renderer.render()
# Render returns None (draws directly on canvas)
assert result is None
def test_render_cell_with_text(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a cell with text content."""
cell = TableCell()
paragraph = Paragraph(sample_font)
paragraph.add_word(Word("Test", sample_font))
paragraph.add_word(Word("Content", sample_font))
cell.add_block(paragraph)
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(200, 50),
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = cell_renderer.render()
assert result is None
def test_render_header_cell(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a header cell with different styling."""
cell = TableCell(is_header=True)
paragraph = Paragraph(sample_font)
paragraph.add_word(Word("Header", sample_font))
cell.add_block(paragraph)
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(200, 50),
draw=sample_draw,
style=default_table_style,
is_header_section=True,
canvas=sample_canvas
)
result = cell_renderer.render()
assert result is None
def test_render_cell_with_heading(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a cell with heading content."""
cell = TableCell()
heading = Heading(HeadingLevel.H2, sample_font)
heading.add_word(Word("Heading", sample_font))
cell.add_block(heading)
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(200, 50),
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = cell_renderer.render()
assert result is None
def test_in_object(self, sample_font, sample_draw, default_table_style):
"""Test in_object method for point detection."""
cell = TableCell()
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(100, 50),
draw=sample_draw,
style=default_table_style
)
# Point inside cell
assert cell_renderer.in_object((50, 30))
# Point outside cell
assert not cell_renderer.in_object((150, 30))
assert not cell_renderer.in_object((50, 100))
def test_properties_access(self, sample_font, sample_draw, default_table_style):
"""Test accessing cell renderer properties."""
import numpy as np
cell = TableCell()
cell_renderer = TableCellRenderer(
cell,
origin=(10, 10),
size=(100, 50),
draw=sample_draw,
style=default_table_style
)
# May be numpy arrays
assert np.array_equal(cell_renderer._origin, (10, 10))
assert np.array_equal(cell_renderer._size, (100, 50))
# ============================================================================
# TableRowRenderer Tests
# ============================================================================
class TestTableRowRenderer:
"""Tests for TableRowRenderer."""
def test_initialization(self, sample_font, sample_draw, default_table_style):
"""Test TableRowRenderer initialization."""
row = TableRow()
column_widths = [100, 150, 200]
row_renderer = TableRowRenderer(
row,
origin=(10, 10),
column_widths=column_widths,
row_height=50,
draw=sample_draw,
style=default_table_style
)
assert row_renderer._row == row
assert row_renderer._column_widths == column_widths
assert row_renderer._row_height == 50
assert row_renderer._draw == sample_draw
assert row_renderer._style == default_table_style
assert row_renderer._is_header_section is False
def test_render_empty_row(self, sample_font, sample_draw, default_table_style):
"""Test rendering an empty row."""
row = TableRow()
row_renderer = TableRowRenderer(
row,
origin=(10, 10),
column_widths=[100, 100],
row_height=50,
draw=sample_draw,
style=default_table_style
)
result = row_renderer.render()
assert result is None
def test_render_row_with_cells(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a row with multiple cells."""
row = TableRow()
# Add cells to row
for i in range(3):
cell = TableCell()
paragraph = Paragraph(sample_font)
paragraph.add_word(Word(f"Cell{i}", sample_font))
cell.add_block(paragraph)
row.add_cell(cell)
row_renderer = TableRowRenderer(
row,
origin=(10, 10),
column_widths=[100, 100, 100],
row_height=50,
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = row_renderer.render()
assert result is None
# Verify cells were created
assert len(row_renderer._cell_renderers) == 3
def test_render_row_with_colspan(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a row with cells that span multiple columns."""
row = TableRow()
# Cell with colspan=2
cell1 = TableCell(colspan=2)
paragraph1 = Paragraph(sample_font)
paragraph1.add_word(Word("Spanning", sample_font))
cell1.add_block(paragraph1)
row.add_cell(cell1)
# Normal cell
cell2 = TableCell()
paragraph2 = Paragraph(sample_font)
paragraph2.add_word(Word("Normal", sample_font))
cell2.add_block(paragraph2)
row.add_cell(cell2)
row_renderer = TableRowRenderer(
row,
origin=(10, 10),
column_widths=[100, 100, 100],
row_height=50,
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = row_renderer.render()
assert result is None
# ============================================================================
# TableRenderer Tests
# ============================================================================
class TestTableRenderer:
"""Tests for TableRenderer."""
def test_initialization(self, simple_table, sample_draw, default_table_style):
"""Test TableRenderer initialization."""
import numpy as np
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
assert table_renderer._table == simple_table
assert np.array_equal(table_renderer._origin, (10, 10))
assert table_renderer._available_width == 600
assert table_renderer._draw == sample_draw
assert table_renderer._style == default_table_style
def test_dimension_calculation(
self,
simple_table,
sample_draw,
default_table_style):
"""Test table dimension calculation."""
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
# Check that dimensions were calculated
assert len(table_renderer._column_widths) == 2
assert len(table_renderer._row_heights) == 3 # header, body, footer
assert all(width > 0 for width in table_renderer._column_widths)
def test_render_simple_table(
self,
simple_table,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a complete simple table."""
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = table_renderer.render()
assert result is None
# Verify rows were created
assert len(table_renderer._row_renderers) == 2 # 1 header + 1 body
def test_render_table_with_caption(
self,
simple_table,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a table with caption."""
simple_table.caption = "Test Table Caption"
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = table_renderer.render()
assert result is None
def test_height_property(self, simple_table, sample_draw, default_table_style):
"""Test table height property."""
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
height = table_renderer.height
assert isinstance(height, int)
assert height > 0
def test_width_property(self, simple_table, sample_draw, default_table_style):
"""Test table width property."""
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
width = table_renderer.width
assert isinstance(width, int)
assert width > 0
assert width <= 600 # Should not exceed available width
def test_empty_table(self, sample_draw, default_table_style):
"""Test rendering an empty table."""
empty_table = Table()
table_renderer = TableRenderer(
empty_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
# Should handle gracefully
assert table_renderer is not None
def test_table_with_footer(
self,
sample_font,
sample_draw,
sample_canvas,
default_table_style):
"""Test rendering a table with footer rows."""
table = Table()
# Add header
header_row = TableRow()
header_cell = TableCell(is_header=True)
header_p = Paragraph(sample_font)
header_p.add_word(Word("Header", sample_font))
header_cell.add_block(header_p)
header_row.add_cell(header_cell)
table.add_row(header_row, section="header")
# Add body
body_row = TableRow()
body_cell = TableCell()
body_p = Paragraph(sample_font)
body_p.add_word(Word("Body", sample_font))
body_cell.add_block(body_p)
body_row.add_cell(body_cell)
table.add_row(body_row, section="body")
# Add footer
footer_row = TableRow()
footer_cell = TableCell()
footer_p = Paragraph(sample_font)
footer_p.add_word(Word("Footer", sample_font))
footer_cell.add_block(footer_p)
footer_row.add_cell(footer_cell)
table.add_row(footer_row, section="footer")
table_renderer = TableRenderer(
table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style,
canvas=sample_canvas
)
result = table_renderer.render()
assert result is None
assert len(table_renderer._row_renderers) == 3 # header + body + footer
def test_in_object(self, simple_table, sample_draw, default_table_style):
"""Test in_object method for table."""
table_renderer = TableRenderer(
simple_table,
origin=(10, 10),
available_width=600,
draw=sample_draw,
style=default_table_style
)
# Point inside table
assert table_renderer.in_object((50, 50))
# Point outside table
assert not table_renderer.in_object((1000, 1000))
if __name__ == "__main__":
pytest.main([__file__, "-v"])