Update coverage badges [skip ci]
This commit is contained in:
@@ -0,0 +1,652 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user