""" 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 (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) 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()