Repository: MyreMylar/pygame_gui
Pull request #309 (2022-08-11 16:21:50)

Improve text submodule test coverage
In a series of commits


Patch:
diff --git a/pygame_gui/core/text/__init__.py b/pygame_gui/core/text/__init__.py
index 363f80f..ec3b867 100644
--- a/pygame_gui/core/text/__init__.py
+++ b/pygame_gui/core/text/__init__.py
@@ -2,6 +2,7 @@ from pygame_gui.core.text.text_layout_rect import TextLayoutRect, TextFloatPosit
 from pygame_gui.core.text.line_break_layout_rect import LineBreakLayoutRect
 from pygame_gui.core.text.horiz_rule_layout_rect import HorizRuleLayoutRect
 from pygame_gui.core.text.simple_test_layout_rect import SimpleTestLayoutRect
+from pygame_gui.core.text.image_layout_rect import ImageLayoutRect
 from pygame_gui.core.text.hyperlink_text_chunk import HyperlinkTextChunk
 from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 from pygame_gui.core.text.text_box_layout import TextBoxLayout
@@ -19,6 +20,7 @@ __all__ = ['TextLayoutRect',
            'SimpleTestLayoutRect',
            'HyperlinkTextChunk',
            'TextLineChunkFTFont',
+           'ImageLayoutRect',
            'HTMLParser',
            'TextBoxLayout',
            'TextBoxLayoutRow',
diff --git a/pygame_gui/core/text/html_parser.py b/pygame_gui/core/text/html_parser.py
index 592e99f..71ece98 100644
--- a/pygame_gui/core/text/html_parser.py
+++ b/pygame_gui/core/text/html_parser.py
@@ -1,7 +1,7 @@
 import warnings
 import html.parser
 from collections import deque
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Tuple
 from pathlib import Path
 from _markupbase import ParserBase
 
@@ -100,7 +100,7 @@ class HTMLParser(html.parser.HTMLParser):
         """
         self.layout_rect_queue.clear()
 
-    def handle_starttag(self, tag: str, attrs: Dict[str, str]):
+    def handle_starttag(self, tag: str, attrs: List[Tuple[str, str]]):
         """
         Process an HTML 'start tag' (e.g. 'b' - tags are stripped of their angle brackets)
         where we have a start and an end tag enclosing a range of text this is the first
@@ -192,7 +192,7 @@ class HTMLParser(html.parser.HTMLParser):
                     warnings.warn('Size of: ' + str(float(attributes['size'])) +
                                   " - is not a supported html style size."
                                   " Try .5 increments between 1 & 7 or use 'pixel_size' instead to "
-                                  "set the font size directly")
+                                  "set the font size directly", category=UserWarning)
                     font_size = self.default_style['font_size']
             else:
                 font_size = None
diff --git a/pygame_gui/core/text/text_effects.py b/pygame_gui/core/text/text_effects.py
index 70053d3..1769783 100644
--- a/pygame_gui/core/text/text_effects.py
+++ b/pygame_gui/core/text/text_effects.py
@@ -87,9 +87,8 @@ class TypingAppearEffect(TextEffect):
         :param time_delta: time in seconds since last frame.
         """
         if self.text_progress < self.text_owner.get_text_letter_count(self.text_sub_chunk):
-            if self.time_per_letter_acc < self.current_time_per_letter:
-                self.time_per_letter_acc += time_delta
-            else:
+            self.time_per_letter_acc += time_delta
+            if self.time_per_letter_acc >= self.current_time_per_letter:
                 self.time_per_letter_acc = 0.0
                 self.text_progress += 1
                 self.text_changed = True
@@ -302,7 +301,12 @@ class BounceEffect(TextEffect):
     def _load_params(self, params: Optional[Dict[str, Any]]):
         if params is not None:
             if 'loop' in params:
-                self.loop = bool(params['loop'])
+                if isinstance(params['loop'], bool):
+                    self.loop = params['loop']
+                elif isinstance(params['loop'], str):
+                    self.loop = bool(int(params['loop']))
+                else:
+                    self.loop = bool(params['loop'])
             if 'bounce_max_height' in params:
                 self.bounce_max_height = int(params['bounce_max_height'])
             if 'time_to_complete_bounce' in params:
@@ -383,7 +387,12 @@ class TiltEffect(TextEffect):
     def _load_params(self, params: Optional[Dict[str, Any]]):
         if params is not None:
             if 'loop' in params:
-                self.loop = bool(params['loop'])
+                if isinstance(params['loop'], bool):
+                    self.loop = params['loop']
+                elif isinstance(params['loop'], str):
+                    self.loop = bool(int(params['loop']))
+                else:
+                    self.loop = bool(params['loop'])
             if 'max_rotation' in params:
                 self.max_rotation = int(params['max_rotation'])
             if 'time_to_complete_rotation' in params:
@@ -463,7 +472,12 @@ class ExpandContractEffect(TextEffect):
     def _load_params(self, params: Optional[Dict[str, Any]]):
         if params is not None:
             if 'loop' in params:
-                self.loop = bool(params['loop'])
+                if isinstance(params['loop'], bool):
+                    self.loop = params['loop']
+                elif isinstance(params['loop'], str):
+                    self.loop = bool(int(params['loop']))
+                else:
+                    self.loop = bool(params['loop'])
             if 'max_scale' in params:
                 self.max_scale = float(params['max_scale'])
             if 'time_to_complete_expand_contract' in params:


Test patch:
diff --git a/tests/test_core/test_text/test_horiz_rule_layout_rect.py b/tests/test_core/test_text/test_horiz_rule_layout_rect.py
index 70b2701..f01cf51 100644
--- a/tests/test_core/test_text/test_horiz_rule_layout_rect.py
+++ b/tests/test_core/test_text/test_horiz_rule_layout_rect.py
@@ -33,6 +33,30 @@ class TestHorizRuleLayoutRect:
         assert rule_surface.get_at((10, 10)) == pygame.Color(255, 255, 255, 255)
         assert rule_surface.get_at((10, 11)) == pygame.Color(255, 255, 255, 63)
 
+        horiz_rule = HorizRuleLayoutRect(height=20,
+                                         colour_or_gradient=pygame.Color('#FFFFFF'),
+                                         rule_dimensions=(100, 5))
+        horiz_rule.width = 200
+
+        horiz_rule.finalise(target_surface=rule_surface,
+                            target_area=pygame.Rect(0, 0, 200, 20),
+                            row_chunk_origin=0,
+                            row_chunk_height=20,
+                            row_bg_height=20)
+
+        horiz_rule = HorizRuleLayoutRect(height=20,
+                                         colour_or_gradient=pygame.Color('#FFFFFF'),
+                                         rule_dimensions=(100, 5),
+                                         alignment=HorizRuleLayoutRect.ALIGN_RIGHT,
+                                         has_shade=False)
+        horiz_rule.width = 200
+
+        horiz_rule.finalise(target_surface=rule_surface,
+                            target_area=pygame.Rect(0, 0, 200, 20),
+                            row_chunk_origin=0,
+                            row_chunk_height=20,
+                            row_bg_height=20)
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_core/test_text/test_html_parser.py b/tests/test_core/test_text/test_html_parser.py
index 8093910..8fc936c 100644
--- a/tests/test_core/test_text/test_html_parser.py
+++ b/tests/test_core/test_text/test_html_parser.py
@@ -4,7 +4,7 @@ import pytest
 
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.core.text import TextLineChunkFTFont
-from pygame_gui.core.text import HTMLParser
+from pygame_gui.core.text import HTMLParser, LineBreakLayoutRect, ImageLayoutRect
 
 
 class TestHTMLParser:
@@ -42,18 +42,64 @@ class TestHTMLParser:
         assert parser.current_style['italic'] is False
         assert parser.current_style['bold'] is False
 
-        parser.handle_starttag(tag='b', attrs=dict([]))
+        parser.handle_starttag(tag='b', attrs=[])
 
         assert len(parser.style_stack) == 2
         assert parser.style_stack[1][0] == 'b'
         assert parser.current_style['bold'] is True
 
-        parser.handle_starttag(tag='i', attrs=dict([]))
+        parser.handle_starttag(tag='i', attrs=[])
 
         assert len(parser.style_stack) == 3
         assert parser.style_stack[2][0] == 'i'
         assert parser.current_style['italic'] is True
 
+        parser.current_style['italic'] = False
+        parser.current_style['bold'] = False
+
+        with pytest.warns(UserWarning, match="Unsupported HTML Tag"):
+            parser.handle_starttag(tag='bad_tag', attrs=[])
+
+        parser.handle_starttag(tag='shadow', attrs=[('color', "link_text")])
+
+        assert (parser.current_style['shadow_data'][3] ==
+                default_ui_manager.get_theme().get_colour_or_gradient("link_text", combined_ids))
+
+        parser.handle_starttag(tag='font', attrs=[('pixel_size', "8")])
+
+        assert parser.current_style['font_size'] == 8
+
+        parser.handle_starttag(tag='font', attrs=[('pixel_size', "")])
+
+        assert parser.current_style['font_size'] is None
+
+        with pytest.warns(UserWarning, match="not a supported html style size"):
+            parser.handle_starttag(tag='font', attrs=[('size', "8")])
+
+        parser.handle_starttag(tag='body', attrs=[('bgcolor', "link_text")])
+
+        assert (parser.current_style['bg_colour'] ==
+                default_ui_manager.get_theme().get_colour_or_gradient("link_text", combined_ids))
+
+        parser.handle_starttag(tag='body', attrs=[('bgcolor', "")])
+
+        assert parser.current_style['bg_colour'] == pygame.Color('#00000000')
+
+        parser.handle_starttag(tag='p', attrs=[])
+        parser.handle_starttag(tag='p', attrs=[])
+
+        assert isinstance(parser.layout_rect_queue[-1], LineBreakLayoutRect)
+
+        parser.handle_starttag(tag='img', attrs=[('src', 'tests/data/images/splat.png'),
+                                                 ('float', 'none'),
+                                                 ('padding', '0 0 0')])
+
+        assert isinstance(parser.layout_rect_queue[-1], ImageLayoutRect)
+        assert parser.layout_rect_queue[-1].padding.top == 0
+        assert parser.layout_rect_queue[-1].padding.bottom == 0
+        assert parser.layout_rect_queue[-1].padding.right == 0
+        assert parser.layout_rect_queue[-1].padding.left == 0
+
     def test_handle_end_tag(self, _init_pygame, default_ui_manager: UIManager):
         combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
                                                                              ['@test_text'],
@@ -71,7 +117,7 @@ class TestHTMLParser:
         assert parser.current_style['italic'] is False
         assert parser.current_style['bold'] is False
 
-        parser.handle_starttag(tag='b', attrs=dict([]))
+        parser.handle_starttag(tag='b', attrs=[])
 
         assert len(parser.style_stack) == 2
         assert parser.style_stack[1][0] == 'b'
@@ -105,7 +151,7 @@ class TestHTMLParser:
         assert parser.layout_rect_queue[0].text == 'blep'
         assert parser.layout_rect_queue[0].underlined is False
 
-        parser.handle_starttag(tag='u', attrs=dict([]))
+        parser.handle_starttag(tag='u', attrs=[])
         parser.handle_data('underlined text')
         parser.handle_endtag(tag='u')
 
@@ -163,6 +209,25 @@ class TestHTMLParser:
         assert len(parser.style_stack) == 1
         assert parser.current_style['bold'] is False
 
+        parser.pop_style('bad_tag')
+
+    def test_error(self, _init_pygame, default_ui_manager: UIManager):
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+                                                                             ['@test_text'],
+                                                                             ['#test_text_1'])
+        link_style = {'link_text': pygame.Color('#80A0F0'),
+                      'link_hover': pygame.Color('#5080C0'),
+                      'link_selected': pygame.Color('#8050C0'),
+                      'link_normal_underline': True,
+                      'link_hover_underline': True}
+        parser = HTMLParser(ui_theme=default_ui_manager.get_theme(),
+                            combined_ids=combined_ids,
+                            link_style=link_style,
+                            line_spacing=1.25)
+
+        with pytest.warns(UserWarning, match="test error"):
+            parser.error("test error")
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_core/test_text/test_simple_test_layout_rect.py b/tests/test_core/test_text/test_simple_test_layout_rect.py
index 8c49414..e78835d 100644
--- a/tests/test_core/test_text/test_simple_test_layout_rect.py
+++ b/tests/test_core/test_text/test_simple_test_layout_rect.py
@@ -57,6 +57,13 @@ class TestSimpleTestLayoutRect:
         assert another_rect.width == 56
         assert another_rect.height == 30
 
+        simple_rect = SimpleTestLayoutRect(dimensions=(200, 30), create_split_points=False)
+
+        assert simple_rect.split_points == []
+
+        with pytest.raises(ValueError, match="Line width is too narrow"):
+            simple_rect.split(requested_x=10, line_width=0, row_start_x=0)
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_core/test_text/test_text_box_layout_row.py b/tests/test_core/test_text/test_text_box_layout_row.py
index b133a0a..b06c066 100644
--- a/tests/test_core/test_text/test_text_box_layout_row.py
+++ b/tests/test_core/test_text/test_text_box_layout_row.py
@@ -4,7 +4,8 @@ import pygame
 import pygame.freetype
 import pytest
 
-from pygame_gui.core.text import TextBoxLayoutRow, TextBoxLayout, SimpleTestLayoutRect, TextLineChunkFTFont
+from pygame_gui.core.text import HTMLParser, TextBoxLayoutRow, TextBoxLayout, TextFloatPosition
+from pygame_gui.core.text import SimpleTestLayoutRect, TextLineChunkFTFont
 from pygame_gui.ui_manager import UIManager
 
 
@@ -118,6 +119,13 @@ class TestTextBoxLayoutRow:
         assert layout_row.items[2].right == 150
         assert layout_row.right == 150
 
+        simple_rect = SimpleTestLayoutRect(dimensions=(20, 30), float_pos=TextFloatPosition.LEFT)
+        simple_rect_2 = SimpleTestLayoutRect(dimensions=(20, 30), float_pos=TextFloatPosition.RIGHT)
+        text_box_layout.floating_rects.append(simple_rect)
+        text_box_layout.floating_rects.append(simple_rect_2)
+
+        layout_row.horiz_center_row(text_box_layout.floating_rects)
+
     def test_align_left_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
@@ -179,6 +187,14 @@ class TestTextBoxLayoutRow:
         assert layout_row.items[2].right == 195
         assert layout_row.right == 195
 
+        simple_rect = SimpleTestLayoutRect(dimensions=(20, 30), float_pos=TextFloatPosition.LEFT)
+        simple_rect_2 = SimpleTestLayoutRect(dimensions=(20, 30), float_pos=TextFloatPosition.RIGHT)
+        text_box_layout.floating_rects.append(simple_rect)
+        text_box_layout.floating_rects.append(simple_rect_2)
+
+        layout_row.align_right_row(floating_rects=text_box_layout.floating_rects,
+                                   start_x=text_box_layout.layout_rect.width - 5)
+
     def test_vert_align_items_to_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
@@ -465,6 +481,10 @@ class TestTextBoxLayoutRow:
         assert layout_row.cursor_index == 3
         assert layout_row.cursor_draw_width == 44
 
+        layout_row.set_cursor_from_click_pos((180, 5))
+
+        assert layout_row.cursor_index == 4
+
     def test_set_cursor_position(self):
         input_data = deque([])
         line_spacing = 1.25
@@ -510,7 +530,20 @@ class TestTextBoxLayoutRow:
         assert layout_row.cursor_index == 3
         assert layout_row.cursor_draw_width == 44
 
-    def test_insert_text(self):
+    def test_insert_text(self, _init_pygame, default_ui_manager: UIManager):
+        combined_ids = default_ui_manager.get_theme().build_all_combined_ids(['text_box'],
+                                                                             ['@test_text'],
+                                                                             ['#test_text_1'])
+        link_style = {'link_text': pygame.Color('#80A0F0'),
+                      'link_hover': pygame.Color('#5080C0'),
+                      'link_selected': pygame.Color('#8050C0'),
+                      'link_normal_underline': True,
+                      'link_hover_underline': True}
+        parser = HTMLParser(ui_theme=default_ui_manager.get_theme(),
+                            combined_ids=combined_ids,
+                            link_style=link_style,
+                            line_spacing=1.25)
+
         input_data = deque([])
         line_spacing = 1.25
         text_box_layout = TextBoxLayout(input_data_queue=input_data,
@@ -523,17 +556,22 @@ class TestTextBoxLayoutRow:
                                       line_spacing=line_spacing,
                                       layout=text_box_layout)
 
-        font_1 = pygame.freetype.Font(None, 30)
-        font_1.origin = True
-        font_1.pad = True
-        text_chunk_1 = TextLineChunkFTFont(text='test',
-                                           font=font_1,
-                                           underlined=False,
-                                           colour=pygame.Color('#FFFFFF'),
-                                           using_default_text_colour=False,
-                                           bg_colour=pygame.Color('#00000000'))
-
-        layout_row.add_item(text_chunk_1)
+        with pytest.raises(AttributeError, match="Trying to insert into empty text row with no Parser"):
+            layout_row.insert_text('bad text', 0)
+
+        layout_row.insert_text('test', 0, parser=parser)
+
+        # font_1 = pygame.freetype.Font(None, 30)
+        # font_1.origin = True
+        # font_1.pad = True
+        # text_chunk_1 = TextLineChunkFTFont(text='test',
+        #                                    font=font_1,
+        #                                    underlined=False,
+        #                                    colour=pygame.Color('#FFFFFF'),
+        #                                    using_default_text_colour=False,
+        #                                    bg_colour=pygame.Color('#00000000'))
+        #
+        # layout_row.add_item(text_chunk_1)
 
         layout_row.insert_text(' this', 4)
 
diff --git a/tests/test_core/test_text/test_text_effects.py b/tests/test_core/test_text/test_text_effects.py
index b3a3cd7..b809c09 100644
--- a/tests/test_core/test_text/test_text_effects.py
+++ b/tests/test_core/test_text/test_text_effects.py
@@ -2,7 +2,7 @@ import pygame
 import pygame.freetype
 import pytest
 
-from pygame_gui.core.text import TypingAppearEffect, FadeInEffect, FadeOutEffect
+from pygame_gui.core.text import TypingAppearEffect, FadeInEffect, FadeOutEffect, TextEffect
 from pygame_gui.core.text import TextLineChunkFTFont
 from pygame_gui.core.text.text_effects import BounceEffect, TiltEffect, ExpandContractEffect
 from pygame_gui.ui_manager import UIManager
@@ -13,6 +13,18 @@ from pygame_gui import TEXT_EFFECT_BOUNCE, TEXT_EFFECT_TILT, TEXT_EFFECT_EXPAND_
 from pygame_gui import UITextEffectType, UI_TEXT_EFFECT_FINISHED
 
 
+class TestTextEffect:
+    def test_get_final_alpha(self, _init_pygame, default_ui_manager: UIManager):
+        text_effect = TextEffect()
+
+        assert text_effect.get_final_alpha() == 255
+
+    def test_has_text_changed(self, _init_pygame, default_ui_manager: UIManager):
+        text_effect = TextEffect()
+
+        assert not text_effect.has_text_changed()
+
+
 class TestTypingAppearEffect:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
         text_box = UITextBox('Hello world', pygame.Rect((10, 10), (200, 100)),
@@ -46,8 +58,6 @@ class TestTypingAppearEffect:
 
         assert text_box.active_text_effect.text_progress == 0
 
-        text_box.active_text_effect.update(time_delta=0.06)
-        text_box.active_text_effect.update(time_delta=0.06)
         text_box.active_text_effect.update(time_delta=0.06)
         text_box.active_text_effect.update(time_delta=0.06)
 
@@ -61,10 +71,6 @@ class TestTypingAppearEffect:
 
         assert label.active_text_effect.text_progress == 0
 
-        label.update(time_delta=0.06)
-        label.update(time_delta=0.06)
-        label.update(time_delta=0.06)
-        label.update(time_delta=0.06)
         label.update(time_delta=0.06)
         label.update(time_delta=0.06)
         label.update(time_delta=0.06)
@@ -72,7 +78,7 @@ class TestTypingAppearEffect:
 
         assert label.active_text_effect.text_progress == 4
 
-        label.stop_finished_effect()
+        label.update(time_delta=0.06)
 
         assert label.active_text_effect is None
 
@@ -253,6 +259,16 @@ class TestFadeInEffect:
             if event.type == UI_TEXT_EFFECT_FINISHED:
                 assert event.effect == TEXT_EFFECT_FADE_IN
 
+    def test_get_final_alpha(self, _init_pygame, default_ui_manager: UIManager):
+        text_box = UITextBox('Hello world',
+                             pygame.Rect((10, 10), (200, 100)),
+                             default_ui_manager)
+
+        text_box.set_active_effect(TEXT_EFFECT_FADE_IN,
+                                   params={'time_per_alpha_change': 19.0})
+
+        assert text_box.active_text_effect.get_final_alpha() == 0
+
 
 class TestFadeOutEffect:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
@@ -342,6 +358,16 @@ class TestFadeOutEffect:
             if event.type == UI_TEXT_EFFECT_FINISHED:
                 assert event.effect == TEXT_EFFECT_FADE_OUT
 
+    def test_get_final_alpha(self, _init_pygame, default_ui_manager: UIManager):
+        text_box = UITextBox('Hello world',
+                             pygame.Rect((10, 10), (200, 100)),
+                             default_ui_manager)
+
+        text_box.set_active_effect(TEXT_EFFECT_FADE_OUT,
+                                   params={'time_per_alpha_change': 19.0})
+
+        assert text_box.active_text_effect.get_final_alpha() == 255
+
 
 class TestBounceEffect:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
@@ -388,6 +414,22 @@ class TestBounceEffect:
 
         assert effect.bounce_height != 0
 
+        text_box = UITextBox('<effect id=test>Hello world</effect>',
+                             pygame.Rect((10, 10), (200, 100)),
+                             default_ui_manager)
+
+        text_box.set_active_effect(TEXT_EFFECT_BOUNCE, params={'loop': "1"}, effect_tag='test')
+        text_box.set_active_effect(TEXT_EFFECT_BOUNCE, params={'loop': 1}, effect_tag='test')
+
+        effect = text_box.active_text_chunk_effects[0]['effect']
+
+        assert effect.bounce_height == 0
+
+        effect.update(time_delta=0.06)
+        effect.update(time_delta=0.06)
+
+        assert effect.bounce_height != 0
+
     def test_has_text_changed(self, _init_pygame, default_ui_manager: UIManager):
         text_box = UITextBox('<effect id=test>Hello world</effect>',
                              pygame.Rect((10, 10), (200, 100)),
@@ -488,6 +530,23 @@ class TestTiltEffect:
 
         assert effect.current_rotation != 0
 
+        text_box = UITextBox('<effect id=test>Hello world</effect>',
+                             pygame.Rect((10, 10), (200, 100)),
+                             default_ui_manager)
+
+        text_box.set_active_effect(TEXT_EFFECT_TILT, params={'loop': "1"}, effect_tag='test')
+        text_box.set_active_effect(TEXT_EFFECT_TILT, params={'loop': 1}, effect_tag='test')
+        text_box.set_active_effect(TEXT_EFFECT_TILT, params={'loop': True}, effect_tag='test')
+
+        effect: TiltEffect = text_box.active_text_chunk_effects[0]['effect']
+
+        assert effect.current_rotation == 0
+
+        effect.update(time_delta=0.06)
+        effect.update(time_delta=0.06)
+
+        assert effect.current_rotation != 0
+
     def test_has_text_changed(self, _init_pygame, default_ui_manager: UIManager):
         text_box = UITextBox('<effect id=test>Hello world</effect>',
                              pygame.Rect((10, 10), (200, 100)),
@@ -579,6 +638,22 @@ class TestExpandContractEffect:
 
         assert effect.current_scale != 1.0
 
+        text_box = UITextBox('<effect id=test>Hello world</effect> other text',
+                             pygame.Rect((10, 10), (200, 100)),
+                             default_ui_manager)
+
+        text_box.set_active_effect(TEXT_EFFECT_EXPAND_CONTRACT, params={'loop': 1}, effect_tag='test')
+        text_box.set_active_effect(TEXT_EFFECT_EXPAND_CONTRACT, params={'loop': "1"}, effect_tag='test')
+
+        effect: ExpandContractEffect = text_box.active_text_chunk_effects[0]['effect']
+
+        assert effect.current_scale == 1.0
+
+        text_box.update(time_delta=0.06)
+        text_box.update(time_delta=0.06)
+
+        assert effect.current_scale != 1.0
+
     def test_has_text_changed(self, _init_pygame, default_ui_manager: UIManager):
         text_box = UITextBox('<effect id=test>Hello world</effect> other text',
                              pygame.Rect((10, 10), (200, 100)),
diff --git a/tests/test_core/test_text/test_text_layout_rect.py b/tests/test_core/test_text/test_text_layout_rect.py
index ec39fd8..52f9103 100644
--- a/tests/test_core/test_text/test_text_layout_rect.py
+++ b/tests/test_core/test_text/test_text_layout_rect.py
@@ -74,6 +74,9 @@ class TestTextLayoutRect:
         with pytest.raises(ValueError, match='Line width is too narrow'):
             layout_rect.split(requested_x=0, line_width=0, row_start_x=0)
 
+        with pytest.raises(ValueError, match='Row start must be 0 or greater'):
+            layout_rect.split(requested_x=50, line_width=50, row_start_x=-20)
+
         new_rect = layout_rect.split(requested_x=50, line_width=50, row_start_x=0)
 
         assert layout_rect.width == 50