Add comprehensive tests for layout and rendering components, enhancing coverage for layout lines, blocks, and document structures

This commit is contained in:
Benedikt Willi 2026-01-11 23:41:20 +01:00
parent 8d2fd3b16e
commit 21e779d281
2 changed files with 373 additions and 40 deletions

View file

@ -1,58 +1,273 @@
"""Tests for layout components."""
import pytest
from unittest.mock import Mock
from src.layout.document import DocumentLayout
from src.layout.block import BlockLayout, LineLayout
from src.layout.inline import TextLayout
import sys
from unittest.mock import Mock, patch, MagicMock
# Mock skia before importing layout modules
mock_skia = MagicMock()
mock_font = MagicMock()
mock_font.measureText = lambda text: len(text) * 7.0 # ~7 pixels per char
mock_typeface = MagicMock()
mock_skia.Typeface.MakeDefault.return_value = mock_typeface
mock_skia.Font.return_value = mock_font
sys.modules['skia'] = mock_skia
from src.layout.document import DocumentLayout, LayoutLine, LayoutBlock
from src.layout.block import BlockLayout, LineLayout, build_block_layout
from src.layout.inline import TextLayout, InlineLayout
from src.parser.html import Element, Text
class TestLayoutLine:
"""Tests for LayoutLine class."""
def test_layout_line_creation(self):
line = LayoutLine("Hello", 20, 100, 14)
assert line.text == "Hello"
assert line.x == 20
assert line.y == 100
assert line.font_size == 14
def test_layout_line_with_char_positions(self):
char_positions = [0.0, 5.0, 10.0, 15.0, 20.0, 25.0]
line = LayoutLine("Hello", 20, 100, 14, char_positions)
assert line.char_positions == char_positions
def test_layout_line_height(self):
line = LayoutLine("Test", 0, 0, 14)
# Height should be based on linespace (font_size * 1.4)
assert line.height == pytest.approx(14 * 1.4, rel=0.1)
class TestLayoutBlock:
"""Tests for LayoutBlock class."""
def test_layout_block_creation(self):
block = LayoutBlock("p")
assert block.tag == "p"
assert block.block_type == "block"
assert block.lines == []
def test_layout_block_with_type(self):
block = LayoutBlock("li", "list-item")
assert block.block_type == "list-item"
class TestDocumentLayout:
"""Tests for DocumentLayout class."""
def test_document_layout_creation(self):
node = Element("html")
layout = DocumentLayout(node)
assert layout.node is node
assert layout.children == []
assert layout.blocks == []
assert layout.lines == []
def test_document_layout_finds_body(self):
# Create HTML structure: html > body > p
html = Element("html")
body = Element("body")
p = Element("p")
p.children = [Text("Hello world")]
body.children = [p]
html.children = [body]
def test_document_layout(self):
node = Element("html")
layout = DocumentLayout(html)
lines = layout.layout(800)
assert len(lines) > 0
assert any("Hello world" in line.text for line in lines)
def test_document_layout_returns_empty_without_body(self):
node = Element("div")
node.children = []
layout = DocumentLayout(node)
result = layout.layout(800, 1.0)
assert result == 800.0
lines = layout.layout(800)
assert lines == []
def test_document_layout_handles_headings(self):
body = Element("body")
h1 = Element("h1")
h1.children = [Text("Title")]
body.children = [h1]
layout = DocumentLayout(body)
lines = layout.layout(800)
assert len(lines) == 1
assert lines[0].text == "Title"
assert lines[0].font_size == 24 # h1 font size
def test_document_layout_handles_paragraphs(self):
body = Element("body")
p = Element("p")
p.children = [Text("Paragraph text")]
body.children = [p]
layout = DocumentLayout(body)
lines = layout.layout(800)
assert len(lines) == 1
assert lines[0].text == "Paragraph text"
assert lines[0].font_size == 14 # p font size
def test_document_layout_handles_lists(self):
body = Element("body")
ul = Element("ul")
li = Element("li")
li.children = [Text("List item")]
ul.children = [li]
body.children = [ul]
layout = DocumentLayout(body)
lines = layout.layout(800)
assert len(lines) == 1
assert "" in lines[0].text # Bullet prefix
assert "List item" in lines[0].text
def test_document_layout_word_wrapping(self):
body = Element("body")
p = Element("p")
# Long text that should wrap
p.children = [Text("This is a very long paragraph that should wrap to multiple lines when the width is narrow enough")]
body.children = [p]
layout = DocumentLayout(body)
lines = layout.layout(200) # Narrow width to force wrapping
assert len(lines) > 1 # Should wrap to multiple lines
def test_document_layout_char_positions(self):
body = Element("body")
p = Element("p")
p.children = [Text("Hello")]
body.children = [p]
layout = DocumentLayout(body)
lines = layout.layout(800)
assert len(lines) == 1
# char_positions should have len(text) + 1 entries (including start at 0)
assert len(lines[0].char_positions) == 6 # "Hello" = 5 chars + 1
assert lines[0].char_positions[0] == 0.0
class TestBlockLayout:
"""Tests for BlockLayout class."""
def test_block_layout_creation(self):
node = Element("div")
layout = BlockLayout(node)
assert layout.node is node
assert layout.children == []
def test_block_layout_with_parent(self):
parent_node = Element("body")
child_node = Element("div")
parent_layout = BlockLayout(parent_node)
child_layout = BlockLayout(child_node, parent=parent_layout)
assert child_layout.parent is parent_layout
def test_block_layout_stores_dimensions(self):
node = Element("div")
layout = BlockLayout(node)
layout.x = 10
layout.y = 20
layout.width = 100
layout.height = 50
assert layout.x == 10
assert layout.y == 20
assert layout.width == 100
assert layout.height == 50
class TestLineLayout:
"""Tests for LineLayout class."""
def test_line_layout_creation(self):
node = Element("span")
layout = LineLayout(node)
assert layout.node is node
layout = LineLayout()
assert layout.words == []
assert layout.x == 0
assert layout.y == 0
def test_line_layout_add_word(self):
layout = LineLayout()
layout.add_word("Hello", 0, 14)
layout.add_word("World", 50, 14)
assert len(layout.words) == 2
class TestTextLayout:
"""Tests for TextLayout class."""
def test_text_layout_creation(self):
node = Text("Hello")
layout = TextLayout(node, "Hello")
assert layout.node is node
assert layout.word == "Hello"
def test_text_layout_length(self):
def test_text_layout_layout_returns_width(self):
node = Text("Test")
layout = TextLayout(node, "Test")
width = layout.layout(14)
assert width > 0
def test_text_layout_dimensions(self):
node = Text("Hi")
layout = TextLayout(node, "Hi")
layout.x = 5
layout.y = 10
assert layout.x == 5
assert layout.y == 10
class TestInlineLayout:
"""Tests for InlineLayout class."""
def test_inline_layout_creation(self):
node = Text("Hello")
layout = TextLayout(node, "Hello")
result = layout.layout()
assert result == 5
layout = InlineLayout(node)
assert layout.node is node
assert layout.children == []
def test_inline_layout_add_word(self):
node = Text("Hello World")
layout = InlineLayout(node)
layout.add_word("Hello", 14)
layout.add_word("World", 14)
assert len(layout.children) == 2
def test_inline_layout_layout(self):
node = Text("Hello World")
layout = InlineLayout(node)
layout.add_word("Hello", 14)
layout.add_word("World", 14)
lines = layout.layout(0, 0, 1000, 14)
# Both words should fit on one line with wide width
assert len(lines) == 1
class TestBuildBlockLayout:
"""Tests for build_block_layout factory function."""
def test_build_block_layout_from_element(self):
node = Element("p")
node.children = [Text("Test paragraph")]
result = build_block_layout(node)
assert result is not None
assert result.node is node
assert result.font_size == 14
def test_build_block_layout_heading(self):
node = Element("h1")
node.children = [Text("Heading")]
result = build_block_layout(node, font_size=24)
assert result.font_size == 24
def test_build_block_layout_list_item(self):
node = Element("li")
node.children = [Text("Item")]
result = build_block_layout(node, block_type="list-item", bullet=True)
assert result.block_type == "list-item"

View file

@ -1,34 +1,114 @@
"""Tests for rendering primitives."""
import pytest
from src.render.paint import PaintCommand, DrawText
import sys
from unittest.mock import Mock, patch, MagicMock
# Mock skia before importing render modules
mock_skia = MagicMock()
mock_font = MagicMock()
mock_font.measureText = lambda text: len(text) * 7.0 # ~7 pixels per char
mock_typeface = MagicMock()
mock_skia.Typeface.MakeDefault.return_value = mock_typeface
mock_skia.Font.return_value = mock_font
sys.modules['skia'] = mock_skia
from src.render.paint import PaintCommand, DrawText, DrawRect, DisplayList
from src.render.composite import CompositedLayer
from src.render.fonts import get_font, linespace
from src.render.fonts import FontCache, get_font, measure_text, linespace
class TestPaintCommands:
"""Tests for paint command base class."""
def test_paint_command_creation(self):
cmd = PaintCommand((0, 0, 100, 100))
assert cmd.rect == (0, 0, 100, 100)
class TestDrawText:
"""Tests for DrawText paint command."""
def test_draw_text_creation(self):
cmd = DrawText(10, 20, "Hello", ("Arial", 12), "black")
cmd = DrawText(10, 20, "Hello", 14)
assert cmd.x == 10
assert cmd.y == 20
assert cmd.text == "Hello"
assert cmd.font == ("Arial", 12)
assert cmd.color == "black"
assert cmd.font_size == 14
def test_draw_text_with_color(self):
cmd = DrawText(0, 0, "Test", 12, color=0xFF0000)
assert cmd.color == 0xFF0000
def test_draw_text_rect_property(self):
cmd = DrawText(10, 20, "Hi", 14)
# Should have a rect based on position and size
assert cmd.rect is not None
assert cmd.rect[0] == 10 # x
class TestDrawRect:
"""Tests for DrawRect paint command."""
def test_draw_rect_creation(self):
cmd = DrawRect(10, 20, 110, 70, color=0x000000)
assert cmd.rect == (10, 20, 110, 70)
def test_draw_rect_with_color(self):
cmd = DrawRect(0, 0, 50, 50, color=0x00FF00)
assert cmd.color == 0x00FF00
def test_draw_rect_fill_mode(self):
cmd_fill = DrawRect(0, 0, 50, 50, color=0x000000, fill=True)
cmd_stroke = DrawRect(0, 0, 50, 50, color=0x000000, fill=False)
assert cmd_fill.fill is True
assert cmd_stroke.fill is False
class TestDisplayList:
"""Tests for DisplayList class."""
def test_display_list_creation(self):
dl = DisplayList()
assert len(dl.commands) == 0
def test_display_list_append(self):
dl = DisplayList()
cmd = DrawText(0, 0, "Test", 14)
dl.append(cmd)
assert len(dl.commands) == 1
assert dl.commands[0] is cmd
def test_display_list_len(self):
dl = DisplayList()
dl.append(DrawText(0, 0, "A", 14))
dl.append(DrawText(0, 20, "B", 14))
assert len(dl) == 2
def test_display_list_iteration(self):
dl = DisplayList()
cmd1 = DrawText(0, 0, "A", 14)
cmd2 = DrawText(0, 20, "B", 14)
dl.append(cmd1)
dl.append(cmd2)
items = list(dl)
assert items == [cmd1, cmd2]
class TestCompositedLayer:
"""Tests for composited layer."""
def test_composited_layer_creation(self):
layer = CompositedLayer()
assert layer.items == []
def test_composited_layer_with_item(self):
item = "mock_item"
layer = CompositedLayer(item)
assert len(layer.items) == 1
assert layer.items[0] == item
def test_add_item(self):
layer = CompositedLayer()
layer.add("item1")
@ -36,20 +116,58 @@ class TestCompositedLayer:
assert len(layer.items) == 2
class TestFonts:
class TestFontCache:
"""Tests for FontCache singleton."""
def test_font_cache_singleton(self):
cache1 = FontCache()
cache2 = FontCache()
assert cache1 is cache2
def test_font_cache_get_font(self):
cache = FontCache()
font1 = cache.get_font(14)
font2 = cache.get_font(14)
# Should return the same cached font
assert font1 is font2
def test_font_cache_different_sizes(self):
cache = FontCache()
font14 = cache.get_font(14)
font18 = cache.get_font(18)
# Different sizes should be different font instances (but both are mocked)
# At minimum, both should be non-None
assert font14 is not None
assert font18 is not None
class TestFontFunctions:
"""Tests for font module functions."""
def test_get_font(self):
font = get_font(14)
assert font == (14, "normal", "normal")
def test_get_font_with_weight(self):
font = get_font(16, weight="bold")
assert font == (16, "bold", "normal")
def test_get_font_with_style(self):
font = get_font(12, style="italic")
assert font == (12, "normal", "italic")
assert font is not None
def test_get_font_caching(self):
font1 = get_font(16)
font2 = get_font(16)
assert font1 is font2
def test_measure_text(self):
width = measure_text("Hello", 14)
assert width > 0
assert isinstance(width, (int, float))
def test_measure_text_empty(self):
width = measure_text("", 14)
assert width == 0
def test_linespace(self):
font = (14, "normal", "normal")
space = linespace(font)
assert space == int(14 * 1.2)
space = linespace(14)
# Should be font_size * 1.4 (typical line height)
assert space == pytest.approx(14 * 1.4, rel=0.1)
def test_linespace_different_sizes(self):
space14 = linespace(14)
space20 = linespace(20)
assert space20 > space14