pyWebLayout/tests/test_abstract_inline.py
Duncan Tourolle dd2c98d4e0
All checks were successful
Python CI / test (push) Successful in 42s
tests for word
2025-06-07 17:43:21 +02:00

1061 lines
41 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, LineBreak
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 (language - 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)
def test_word_create_and_add_to_with_container_style(self):
"""Test Word.create_and_add_to with container that has style property."""
# Create mock container with style and add_word method
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Ensure _words and background don't interfere
del mock_container._words
del mock_container.background # Remove background so it inherits from font
# Create and add word
word = Word.create_and_add_to("hello", mock_container)
# Test that word was created with correct properties
self.assertIsInstance(word, Word)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
# Test that add_word was called
mock_container.add_word.assert_called_once_with(word)
def test_word_create_and_add_to_with_style_override(self):
"""Test Word.create_and_add_to with explicit style parameter."""
# Create alternate font
alt_font = Font()
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word with style override
word = Word.create_and_add_to("test", mock_container, style=alt_font)
# Test that word uses the override style, not container style
self.assertEqual(word.style, alt_font)
self.assertNotEqual(word.style, self.font)
def test_word_create_and_add_to_with_background_override(self):
"""Test Word.create_and_add_to with explicit background parameter."""
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word with background override
word = Word.create_and_add_to("test", mock_container, background="override_bg")
# Test that word uses the override background
self.assertEqual(word.background, "override_bg")
def test_word_create_and_add_to_inherit_container_background(self):
"""Test Word.create_and_add_to inheriting background from container."""
# Create mock container with background
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_word = Mock()
# Ensure _words doesn't interfere
del mock_container._words
# Create word without background override
word = Word.create_and_add_to("test", mock_container)
# Test that word inherits container background
self.assertEqual(word.background, "container_bg")
def test_word_create_and_add_to_with_words_list_previous(self):
"""Test Word.create_and_add_to linking with previous word from _words list."""
# Create mock container with _words list
mock_container = Mock()
mock_container.style = self.font
mock_container.add_word = Mock()
# Create existing word and add to container's _words list
existing_word = Word("previous", self.font)
mock_container._words = [existing_word]
# Create new word
word = Word.create_and_add_to("current", mock_container)
# Test that words are linked
self.assertEqual(word.previous, existing_word)
self.assertEqual(existing_word.next, word)
def test_word_create_and_add_to_with_words_method_previous(self):
"""Test Word.create_and_add_to linking with previous word from words() method."""
# Create a simple container that implements words() method
class SimpleContainer:
def __init__(self, font):
self.style = font
self.existing_word = Word("previous", font)
def words(self):
yield ("span1", self.existing_word)
def add_word(self, word):
pass # Simple implementation
container = SimpleContainer(self.font)
# Create new word
word = Word.create_and_add_to("current", container)
# Test that words are linked
self.assertEqual(word.previous, container.existing_word)
self.assertEqual(container.existing_word.next, word)
def test_word_create_and_add_to_no_style_error(self):
"""Test Word.create_and_add_to raises error when container has no style."""
# Create container without style
class BadContainer:
def add_word(self, word):
pass
container = BadContainer()
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
Word.create_and_add_to("test", container)
self.assertIn("must have a 'style' property", str(context.exception))
def test_word_create_and_add_to_no_add_word_error(self):
"""Test Word.create_and_add_to raises error when container has no add_word method."""
# Create container without add_word
class BadContainer:
def __init__(self, font):
self.style = font
container = BadContainer(self.font)
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
Word.create_and_add_to("test", container)
self.assertIn("must have an 'add_word' method", str(context.exception))
def test_word_create_and_add_to_parameter_inspection_word_object(self):
"""Test Word.create_and_add_to with add_word method that expects Word object."""
# Create container with add_word method that has 'word' parameter name
class WordObjectContainer:
def __init__(self, font):
self.style = font
self.added_words = []
def add_word(self, word): # Parameter named 'word' indicates it expects Word object
self.added_words.append(word)
container = WordObjectContainer(self.font)
# Create and add word
word = Word.create_and_add_to("test", container)
# Test that the Word object was passed to add_word
self.assertEqual(len(container.added_words), 1)
self.assertEqual(container.added_words[0], word)
self.assertIsInstance(container.added_words[0], Word)
def test_word_create_and_add_to_parameter_inspection_word_obj(self):
"""Test Word.create_and_add_to with add_word method that has 'word_obj' parameter."""
class WordObjContainer:
def __init__(self, font):
self.style = font
self.added_words = []
def add_word(self, word_obj): # Parameter named 'word_obj' indicates it expects Word object
self.added_words.append(word_obj)
container = WordObjContainer(self.font)
word = Word.create_and_add_to("test", container)
self.assertEqual(len(container.added_words), 1)
self.assertEqual(container.added_words[0], word)
def test_word_create_and_add_to_parameter_inspection_word_object(self):
"""Test Word.create_and_add_to with add_word method that has 'word_object' parameter."""
class WordObjectContainer:
def __init__(self, font):
self.style = font
self.added_words = []
def add_word(self, word_object): # Parameter named 'word_object' indicates it expects Word object
self.added_words.append(word_object)
container = WordObjectContainer(self.font)
word = Word.create_and_add_to("test", container)
self.assertEqual(len(container.added_words), 1)
self.assertEqual(container.added_words[0], word)
def test_word_create_and_add_to_parameter_inspection_text_fallback_with_words_list(self):
"""Test Word.create_and_add_to with add_word that expects text but container has _words list."""
class TextExpectingContainer:
def __init__(self, font):
self.style = font
self._words = [] # Has _words list
self.add_word_calls = []
def add_word(self, text): # Parameter named 'text' suggests it expects string
# This simulates FormattedSpan.add_word behavior
self.add_word_calls.append(text)
# In real FormattedSpan, this would create a Word internally
container = TextExpectingContainer(self.font)
word = Word.create_and_add_to("test", container)
# Word should be added to _words list directly, not via add_word
self.assertEqual(len(container._words), 1)
self.assertEqual(container._words[0], word)
# add_word should not have been called since it expects text
self.assertEqual(len(container.add_word_calls), 0)
def test_word_create_and_add_to_parameter_inspection_fallback_without_words_list(self):
"""Test Word.create_and_add_to fallback when container doesn't have _words list."""
class TextExpectingContainer:
def __init__(self, font):
self.style = font
# No _words list
self.added_words = []
def add_word(self, text): # Parameter suggests text but we'll pass Word as fallback
self.added_words.append(text)
container = TextExpectingContainer(self.font)
word = Word.create_and_add_to("test", container)
# Should fallback to calling add_word with Word object
self.assertEqual(len(container.added_words), 1)
self.assertEqual(container.added_words[0], word)
def test_word_create_and_add_to_no_parameters_edge_case(self):
"""Test Word.create_and_add_to with add_word method that has no parameters."""
class NoParamsContainer:
def __init__(self, font):
self.style = font
self.add_word_called = False
def add_word(self): # No parameters - edge case
self.add_word_called = True
container = NoParamsContainer(self.font)
# The current implementation will fail when calling add_word(word) with a no-parameter method
with self.assertRaises(TypeError) as context:
Word.create_and_add_to("test", container)
self.assertIn("takes 1 positional argument but 2 were given", str(context.exception))
def test_word_create_and_add_to_linking_behavior_with_existing_words(self):
"""Test Word.create_and_add_to properly links with existing words in container."""
# Create container with existing words
class ContainerWithWords:
def __init__(self, font):
self.style = font
self._words = []
# Add an existing word
existing_word = Word("existing", font)
self._words.append(existing_word)
def add_word(self, word):
self._words.append(word)
container = ContainerWithWords(self.font)
existing_word = container._words[0]
# Create new word
new_word = Word.create_and_add_to("new", container)
# Test linking
self.assertEqual(new_word.previous, existing_word)
self.assertEqual(existing_word.next, new_word)
self.assertEqual(len(container._words), 2)
self.assertEqual(container._words[1], new_word)
def test_word_create_and_add_to_linking_behavior_with_words_method(self):
"""Test Word.create_and_add_to properly links with words from container.words() method."""
class ContainerWithWordsMethod:
def __init__(self, font):
self.style = font
self.existing_word1 = Word("first", font)
self.existing_word2 = Word("second", font)
self.existing_word1.add_next(self.existing_word2)
self.added_words = []
def words(self):
yield ("span1", self.existing_word1)
yield ("span2", self.existing_word2)
def add_word(self, word):
self.added_words.append(word)
container = ContainerWithWordsMethod(self.font)
# Create new word
new_word = Word.create_and_add_to("third", container)
# Should link to the last word returned by words() method
self.assertEqual(new_word.previous, container.existing_word2)
self.assertEqual(container.existing_word2.next, new_word)
def test_word_create_and_add_to_linking_behavior_empty_words_method(self):
"""Test Word.create_and_add_to with empty words() method."""
class EmptyWordsContainer:
def __init__(self, font):
self.style = font
def words(self):
# Empty iterator
return iter([])
def add_word(self, word):
pass
container = EmptyWordsContainer(self.font)
# Create word
word = Word.create_and_add_to("test", container)
# Should have no previous word
self.assertIsNone(word.previous)
def test_word_create_and_add_to_linking_behavior_words_method_exception(self):
"""Test Word.create_and_add_to with words() method that raises exception."""
class ExceptionWordsContainer:
def __init__(self, font):
self.style = font
def words(self):
raise TypeError("Error in words method")
def add_word(self, word):
pass
container = ExceptionWordsContainer(self.font)
# Create word - should handle exception gracefully
word = Word.create_and_add_to("test", container)
# Should have no previous word due to exception
self.assertIsNone(word.previous)
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)
def test_formatted_span_create_and_add_to_with_container_style(self):
"""Test FormattedSpan.create_and_add_to with container that has style property."""
# Create mock container with style and add_span method
mock_container = Mock()
mock_container.style = self.font
mock_container.add_span = Mock()
# Remove background so it inherits from font
del mock_container.background
# Create and add span
span = FormattedSpan.create_and_add_to(mock_container)
# Test that span was created with correct properties
self.assertIsInstance(span, FormattedSpan)
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, self.font.background)
# Test that add_span was called
mock_container.add_span.assert_called_once_with(span)
def test_formatted_span_create_and_add_to_with_style_override(self):
"""Test FormattedSpan.create_and_add_to with explicit style parameter."""
# Create alternate font
alt_font = Font()
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.add_span = Mock()
# Create span with style override
span = FormattedSpan.create_and_add_to(mock_container, style=alt_font)
# Test that span uses the override style, not container style
self.assertEqual(span.style, alt_font)
self.assertNotEqual(span.style, self.font)
def test_formatted_span_create_and_add_to_with_background_override(self):
"""Test FormattedSpan.create_and_add_to with explicit background parameter."""
# Create mock container
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_span = Mock()
# Create span with background override
span = FormattedSpan.create_and_add_to(mock_container, background="override_bg")
# Test that span uses the override background
self.assertEqual(span.background, "override_bg")
def test_formatted_span_create_and_add_to_inherit_container_background(self):
"""Test FormattedSpan.create_and_add_to inheriting background from container."""
# Create mock container with background
mock_container = Mock()
mock_container.style = self.font
mock_container.background = "container_bg"
mock_container.add_span = Mock()
# Create span without background override
span = FormattedSpan.create_and_add_to(mock_container)
# Test that span inherits container background
self.assertEqual(span.background, "container_bg")
def test_formatted_span_create_and_add_to_no_style_error(self):
"""Test FormattedSpan.create_and_add_to raises error when container has no style."""
# Create mock container without style
mock_container = Mock()
del mock_container.style
mock_container.add_span = Mock()
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
FormattedSpan.create_and_add_to(mock_container)
self.assertIn("must have a 'style' property", str(context.exception))
def test_formatted_span_create_and_add_to_no_add_span_error(self):
"""Test FormattedSpan.create_and_add_to raises error when container has no add_span method."""
# Create mock container without add_span
mock_container = Mock()
mock_container.style = self.font
del mock_container.add_span
# Test that AttributeError is raised
with self.assertRaises(AttributeError) as context:
FormattedSpan.create_and_add_to(mock_container)
self.assertIn("must have an 'add_span' method", str(context.exception))
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")
class TestLineBreak(unittest.TestCase):
"""Test cases for LineBreak class."""
def test_line_break_creation(self):
"""Test line break creation."""
line_break = LineBreak()
# Test initial state
self.assertIsNotNone(line_break.block_type)
self.assertIsNone(line_break.parent)
def test_line_break_block_type(self):
"""Test line break block type property."""
line_break = LineBreak()
# Import BlockType to verify the correct type
from pyWebLayout.abstract.block import BlockType
self.assertEqual(line_break.block_type, BlockType.LINE_BREAK)
def test_line_break_parent_property(self):
"""Test line break parent property getter and setter."""
line_break = LineBreak()
# Test initial state
self.assertIsNone(line_break.parent)
# Test setter
mock_parent = Mock()
line_break.parent = mock_parent
self.assertEqual(line_break.parent, mock_parent)
# Test setting to None
line_break.parent = None
self.assertIsNone(line_break.parent)
def test_line_break_create_and_add_to_with_add_line_break(self):
"""Test LineBreak.create_and_add_to with container that has add_line_break method."""
# Create mock container with add_line_break method
mock_container = Mock()
mock_container.add_line_break = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_line_break was called
mock_container.add_line_break.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_with_add_element(self):
"""Test LineBreak.create_and_add_to with container that has add_element method."""
# Create mock container without add_line_break but with add_element
mock_container = Mock()
del mock_container.add_line_break # Ensure no add_line_break method
mock_container.add_element = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_element was called
mock_container.add_element.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_with_add_word(self):
"""Test LineBreak.create_and_add_to with container that has add_word method."""
# Create mock container with only add_word method
mock_container = Mock()
del mock_container.add_line_break # Ensure no add_line_break method
del mock_container.add_element # Ensure no add_element method
mock_container.add_word = Mock()
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that add_word was called
mock_container.add_word.assert_called_once_with(line_break)
def test_line_break_create_and_add_to_fallback(self):
"""Test LineBreak.create_and_add_to fallback behavior when no add methods available."""
# Create mock container without any add methods
mock_container = Mock()
del mock_container.add_line_break
del mock_container.add_element
del mock_container.add_word
# Create and add line break
line_break = LineBreak.create_and_add_to(mock_container)
# Test that line break was created
self.assertIsInstance(line_break, LineBreak)
# Test that parent was set manually
self.assertEqual(line_break.parent, mock_container)
if __name__ == '__main__':
unittest.main()