mirror of
https://github.com/Hopiu/bowser.git
synced 2026-03-16 19:10:24 +00:00
Add comprehensive tests for layout and rendering components, enhancing coverage for layout lines, blocks, and document structures
This commit is contained in:
parent
8d2fd3b16e
commit
21e779d281
2 changed files with 373 additions and 40 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue