pyWebLayout/tests/test_abstract_inline.py
Duncan Tourolle c0c366e9f4
Some checks failed
Python CI / test (push) Failing after 1m35s
Additional changes.
2025-06-06 20:54:33 +02:00

521 lines
19 KiB
Python

"""
Unit tests for abstract inline elements.
Tests the Word and FormattedSpan classes that handle inline text elements
and formatting within documents.
"""
import unittest
from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.abstract.inline import Word, FormattedSpan
from pyWebLayout.style import Font
class TestWord(unittest.TestCase):
"""Test cases for Word class."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Note: Font background is a tuple (255, 255, 255, 0) by default
# Note: Font language is set via constructor parameter (langauge - with typo)
def test_word_creation_minimal(self):
"""Test word creation with minimal parameters."""
word = Word("hello", self.font)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
self.assertIsNone(word.previous)
self.assertIsNone(word.next)
self.assertIsNone(word.hyphenated_parts)
def test_word_creation_with_previous(self):
"""Test word creation with previous word reference."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, previous=word1)
self.assertEqual(word2.previous, word1)
self.assertIsNone(word1.previous)
self.assertIsNone(word1.next)
self.assertIsNone(word2.next)
def test_word_creation_with_background_override(self):
"""Test word creation with background color override."""
word = Word("test", self.font, background="yellow")
self.assertEqual(word.background, "yellow")
# Original font background should be unchanged - it's a tuple
self.assertEqual(word.style.background, (255, 255, 255, 0))
def test_word_properties(self):
"""Test word property getters."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, background="blue", previous=word1)
# Test all properties
self.assertEqual(word2.text, "second")
self.assertEqual(word2.style, self.font)
self.assertEqual(word2.background, "blue")
self.assertEqual(word2.previous, word1)
self.assertIsNone(word2.next)
self.assertIsNone(word2.hyphenated_parts)
def test_add_next_word(self):
"""Test linking words with add_next method."""
word1 = Word("first", self.font)
word2 = Word("second", self.font)
word3 = Word("third", self.font)
# Link words
word1.add_next(word2)
word2.add_next(word3)
# Test forward links
self.assertEqual(word1.next, word2)
self.assertEqual(word2.next, word3)
self.assertIsNone(word3.next)
# Backward links should remain as set in constructor
self.assertIsNone(word1.previous)
self.assertIsNone(word2.previous)
self.assertIsNone(word3.previous)
def test_word_chain(self):
"""Test creating a chain of linked words."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, previous=word1)
word3 = Word("third", self.font, previous=word2)
# Add forward links
word1.add_next(word2)
word2.add_next(word3)
# Test complete chain
self.assertIsNone(word1.previous)
self.assertEqual(word1.next, word2)
self.assertEqual(word2.previous, word1)
self.assertEqual(word2.next, word3)
self.assertEqual(word3.previous, word2)
self.assertIsNone(word3.next)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_true(self, mock_pyphen):
"""Test can_hyphenate method when word can be hyphenated."""
# Mock pyphen behavior
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen-ated"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphenated", self.font)
result = word.can_hyphenate()
self.assertTrue(result)
# Font language is set as "en_EN" by default (with typo in constructor param)
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
mock_dic.inserted.assert_called_once_with("hyphenated", hyphen='-')
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_false(self, mock_pyphen):
"""Test can_hyphenate method when word cannot be hyphenated."""
# Mock pyphen behavior for non-hyphenatable word
mock_dic = Mock()
mock_dic.inserted.return_value = "cat" # No hyphens added
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("cat", self.font)
result = word.can_hyphenate()
self.assertFalse(result)
mock_dic.inserted.assert_called_once_with("cat", hyphen='-')
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_with_language_override(self, mock_pyphen):
"""Test can_hyphenate with explicit language parameter."""
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphen", self.font)
result = word.can_hyphenate("de_DE")
self.assertTrue(result)
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_success(self, mock_pyphen):
"""Test successful word hyphenation."""
# Mock pyphen behavior
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen-ation"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphenation", self.font)
result = word.hyphenate()
self.assertTrue(result)
self.assertEqual(word.hyphenated_parts, ["hy-", "phen-", "ation"])
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_failure(self, mock_pyphen):
"""Test word hyphenation when word cannot be hyphenated."""
# Mock pyphen behavior for non-hyphenatable word
mock_dic = Mock()
mock_dic.inserted.return_value = "cat" # No hyphens
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("cat", self.font)
result = word.hyphenate()
self.assertFalse(result)
self.assertIsNone(word.hyphenated_parts)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_with_language_override(self, mock_pyphen):
"""Test hyphenation with explicit language parameter."""
mock_dic = Mock()
mock_dic.inserted.return_value = "Wort-teil"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("Wortteil", self.font)
result = word.hyphenate("de_DE")
self.assertTrue(result)
self.assertEqual(word.hyphenated_parts, ["Wort-", "teil"])
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
def test_dehyphenate(self):
"""Test removing hyphenation from word."""
word = Word("test", self.font)
# Simulate hyphenated state
word._hyphenated_parts = ["test-", "ing"]
word.dehyphenate()
self.assertIsNone(word.hyphenated_parts)
def test_get_hyphenated_part(self):
"""Test getting specific hyphenated parts."""
word = Word("testing", self.font)
# Simulate hyphenated state
word._hyphenated_parts = ["test-", "ing"]
# Test valid indices
self.assertEqual(word.get_hyphenated_part(0), "test-")
self.assertEqual(word.get_hyphenated_part(1), "ing")
# Test invalid index
with self.assertRaises(IndexError):
word.get_hyphenated_part(2)
def test_get_hyphenated_part_not_hyphenated(self):
"""Test getting hyphenated part from non-hyphenated word."""
word = Word("test", self.font)
with self.assertRaises(IndexError) as context:
word.get_hyphenated_part(0)
self.assertIn("Word has not been hyphenated", str(context.exception))
def test_get_hyphenated_part_count(self):
"""Test getting hyphenated part count."""
word = Word("test", self.font)
# Test non-hyphenated word
self.assertEqual(word.get_hyphenated_part_count(), 0)
# Test hyphenated word
word._hyphenated_parts = ["hy-", "phen-", "ated"]
self.assertEqual(word.get_hyphenated_part_count(), 3)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_complex_hyphenation_scenario(self, mock_pyphen):
"""Test complex hyphenation with multiple syllables."""
# Mock pyphen for a complex word
mock_dic = Mock()
mock_dic.inserted.return_value = "un-der-stand-ing"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("understanding", self.font)
result = word.hyphenate()
self.assertTrue(result)
expected_parts = ["un-", "der-", "stand-", "ing"]
self.assertEqual(word.hyphenated_parts, expected_parts)
self.assertEqual(word.get_hyphenated_part_count(), 4)
# Test getting individual parts
for i, expected_part in enumerate(expected_parts):
self.assertEqual(word.get_hyphenated_part(i), expected_part)
class TestFormattedSpan(unittest.TestCase):
"""Test cases for FormattedSpan class."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Font background is a tuple, not a string
def test_formatted_span_creation_minimal(self):
"""Test formatted span creation with minimal parameters."""
span = FormattedSpan(self.font)
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, self.font.background)
self.assertEqual(len(span.words), 0)
def test_formatted_span_creation_with_background(self):
"""Test formatted span creation with background override."""
span = FormattedSpan(self.font, background="yellow")
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, "yellow")
self.assertNotEqual(span.background, self.font.background)
def test_formatted_span_properties(self):
"""Test formatted span property getters."""
span = FormattedSpan(self.font, background="blue")
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, "blue")
self.assertIsInstance(span.words, list)
self.assertEqual(len(span.words), 0)
def test_add_single_word(self):
"""Test adding a single word to formatted span."""
span = FormattedSpan(self.font)
word = span.add_word("hello")
# Test returned word
self.assertIsInstance(word, Word)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
self.assertIsNone(word.previous)
# Test span state
self.assertEqual(len(span.words), 1)
self.assertEqual(span.words[0], word)
def test_add_multiple_words(self):
"""Test adding multiple words to formatted span."""
span = FormattedSpan(self.font)
word1 = span.add_word("first")
word2 = span.add_word("second")
word3 = span.add_word("third")
# Test span contains all words
self.assertEqual(len(span.words), 3)
self.assertEqual(span.words[0], word1)
self.assertEqual(span.words[1], word2)
self.assertEqual(span.words[2], word3)
# Test word linking
self.assertIsNone(word1.previous)
self.assertEqual(word1.next, word2)
self.assertEqual(word2.previous, word1)
self.assertEqual(word2.next, word3)
self.assertEqual(word3.previous, word2)
self.assertIsNone(word3.next)
def test_add_word_with_background_override(self):
"""Test that added words inherit span background."""
span = FormattedSpan(self.font, background="red")
word = span.add_word("test")
# Word should inherit span's background
self.assertEqual(word.background, "red")
self.assertEqual(word.style, self.font)
def test_word_style_consistency(self):
"""Test that all words in span have consistent style."""
span = FormattedSpan(self.font, background="green")
words = []
for text in ["this", "is", "a", "test"]:
words.append(span.add_word(text))
# All words should have same style and background
for word in words:
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, "green")
def test_word_chain_integrity(self):
"""Test that word chain is properly maintained."""
span = FormattedSpan(self.font)
words = []
for i in range(5):
words.append(span.add_word(f"word{i}"))
# Test complete chain
for i in range(5):
word = words[i]
# Test previous link
if i == 0:
self.assertIsNone(word.previous)
else:
self.assertEqual(word.previous, words[i-1])
# Test next link
if i == 4:
self.assertIsNone(word.next)
else:
self.assertEqual(word.next, words[i+1])
def test_empty_span_operations(self):
"""Test operations on empty formatted span."""
span = FormattedSpan(self.font)
# Test empty state
self.assertEqual(len(span.words), 0)
self.assertEqual(span.words, [])
# Add first word
first_word = span.add_word("first")
self.assertIsNone(first_word.previous)
self.assertIsNone(first_word.next)
class TestWordFormattedSpanIntegration(unittest.TestCase):
"""Integration tests between Word and FormattedSpan classes."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Font background is a tuple, not a string
def test_sentence_creation(self):
"""Test creating a complete sentence with formatted span."""
span = FormattedSpan(self.font)
sentence_words = ["The", "quick", "brown", "fox", "jumps"]
words = []
for word_text in sentence_words:
words.append(span.add_word(word_text))
# Test sentence structure
self.assertEqual(len(span.words), 5)
# Test word content
for i, expected_text in enumerate(sentence_words):
self.assertEqual(words[i].text, expected_text)
# Test linking
for i in range(5):
if i > 0:
self.assertEqual(words[i].previous, words[i-1])
if i < 4:
self.assertEqual(words[i].next, words[i+1])
@patch('pyWebLayout.abstract.inline.pyphen')
def test_span_with_hyphenated_words(self, mock_pyphen):
"""Test formatted span containing hyphenated words."""
# Mock pyphen
mock_dic = Mock()
mock_pyphen.Pyphen.return_value = mock_dic
def mock_inserted(word, hyphen='-'):
if word == "understanding":
return "un-der-stand-ing"
elif word == "hyphenation":
return "hy-phen-ation"
else:
return word # No hyphenation
mock_dic.inserted.side_effect = mock_inserted
span = FormattedSpan(self.font)
# Add words, some of which can be hyphenated
word1 = span.add_word("The")
word2 = span.add_word("understanding")
word3 = span.add_word("of")
word4 = span.add_word("hyphenation")
# Test hyphenation
self.assertTrue(word2.can_hyphenate())
self.assertTrue(word2.hyphenate())
self.assertFalse(word1.can_hyphenate())
self.assertTrue(word4.can_hyphenate())
self.assertTrue(word4.hyphenate())
# Test hyphenated parts
self.assertEqual(word2.hyphenated_parts, ["un-", "der-", "stand-", "ing"])
self.assertEqual(word4.hyphenated_parts, ["hy-", "phen-", "ation"])
self.assertIsNone(word1.hyphenated_parts)
self.assertIsNone(word3.hyphenated_parts)
def test_multiple_spans_same_style(self):
"""Test creating multiple spans with the same style."""
font = Font()
span1 = FormattedSpan(font)
span2 = FormattedSpan(font)
# Add words to both spans
span1_words = [span1.add_word("First"), span1.add_word("span")]
span2_words = [span2.add_word("Second"), span2.add_word("span")]
# Test that spans are independent
self.assertEqual(len(span1.words), 2)
self.assertEqual(len(span2.words), 2)
# Test that words in different spans are not linked
self.assertIsNone(span1_words[1].next)
self.assertIsNone(span2_words[0].previous)
# But words within spans are linked
self.assertEqual(span1_words[0].next, span1_words[1])
self.assertEqual(span2_words[1].previous, span2_words[0])
def test_span_style_inheritance(self):
"""Test that words properly inherit span styling."""
font = Font()
# Font background is a tuple (255, 255, 255, 0)
# Test with span background override
span = FormattedSpan(font, background="lightgreen")
word1 = span.add_word("styled")
word2 = span.add_word("text")
# Both words should have span's background, not font's
self.assertEqual(word1.background, "lightgreen")
self.assertEqual(word2.background, "lightgreen")
# But they should have font's other properties
self.assertEqual(word1.style, font)
self.assertEqual(word2.style, font)
def test_word_modification_after_creation(self):
"""Test modifying words after they've been added to span."""
span = FormattedSpan(self.font)
word = span.add_word("original")
# Verify initial state
self.assertEqual(word.text, "original")
self.assertEqual(len(span.words), 1)
# Words are immutable by design (no setter for text property)
# But we can test that the reference is maintained
self.assertEqual(span.words[0], word)
self.assertEqual(span.words[0].text, "original")
if __name__ == '__main__':
unittest.main()