This commit is contained in:
2025-10-10 18:42:13 +02:00
parent 596f640912
commit 6253de27e2
20 changed files with 1327 additions and 12 deletions

View File

@@ -0,0 +1,180 @@
"""
Unit tests for version-aware text processing behavior.
Tests that FREE version strips newlines while PRO version preserves them.
"""
import pytest
from unittest.mock import patch
class TestVersionAwareNewlineHandling:
"""Test that text processing behavior differs between FREE and PRO versions"""
@patch('src.core.text_processor.VERSION_TYPE', 'free')
def test_process_text_strips_newlines_in_free_version(self):
"""FREE version should strip newlines and replace with spaces"""
from src.core.text_processor import process_text_content
text = "Line 1\nLine 2\nLine 3"
result = process_text_content(text)
assert result == "Line 1 Line 2 Line 3", \
f"FREE version must strip newlines, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'full')
def test_process_text_preserves_newlines_in_pro_version(self):
"""PRO version should preserve newlines for multiline rendering"""
from src.core.text_processor import process_text_content
text = "Line 1\nLine 2\nLine 3"
result = process_text_content(text)
assert result == "Line 1\nLine 2\nLine 3", \
f"PRO version must preserve newlines, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'free')
def test_process_text_strips_multiple_consecutive_newlines_in_free(self):
"""FREE version should collapse multiple newlines to single space"""
from src.core.text_processor import process_text_content
text = "Line 1\n\n\nLine 2"
result = process_text_content(text)
assert result == "Line 1 Line 2", \
f"FREE version must collapse multiple newlines, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'free')
def test_process_text_handles_mixed_whitespace_in_free(self):
"""FREE version should normalize mixed whitespace with newlines"""
from src.core.text_processor import process_text_content
text = "Line 1\n \nLine 2"
result = process_text_content(text)
# Should have no newlines and normalized spacing
assert "\n" not in result, \
f"FREE version must remove all newlines, got: {repr(result)}"
assert "Line 1" in result and "Line 2" in result, \
f"Must preserve actual text content, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'full')
def test_process_text_preserves_multiple_newlines_in_pro(self):
"""PRO version should preserve multiple consecutive newlines"""
from src.core.text_processor import process_text_content
text = "Line 1\n\n\nLine 2"
result = process_text_content(text)
assert result == "Line 1\n\n\nLine 2", \
f"PRO version must preserve all newlines, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'free')
def test_process_empty_text_in_free(self):
"""FREE version should handle empty text gracefully"""
from src.core.text_processor import process_text_content
result = process_text_content("")
assert result == "", \
f"Empty text should remain empty, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'free')
def test_process_text_without_newlines_in_free(self):
"""FREE version should pass through text without newlines unchanged"""
from src.core.text_processor import process_text_content
text = "Single line text"
result = process_text_content(text)
assert result == "Single line text", \
f"Text without newlines should be unchanged, got: {repr(result)}"
"""
Unit tests for version-aware text processing behavior.
Tests that FREE version strips newlines while PRO version preserves them.
"""
import pytest
from unittest.mock import patch
class TestVersionAwareNewlineHandling:
"""Test that text processing behavior differs between FREE and PRO versions"""
@patch('src.utils.constants.VERSION_TYPE', 'free')
def test_process_text_strips_newlines_in_free_version(self):
"""FREE version should strip newlines and replace with spaces"""
from src.core.text_processor import process_text_content
text = "Line 1\nLine 2\nLine 3"
result = process_text_content(text)
assert result == "Line 1 Line 2 Line 3", \
f"FREE version must strip newlines, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'full')
def test_process_text_preserves_newlines_in_pro_version(self):
"""PRO version should preserve newlines for multiline rendering"""
from src.core.text_processor import process_text_content
text = "Line 1\nLine 2\nLine 3"
result = process_text_content(text)
assert result == "Line 1\nLine 2\nLine 3", \
f"PRO version must preserve newlines, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'free')
def test_process_text_strips_multiple_consecutive_newlines_in_free(self):
"""FREE version should collapse multiple newlines to single space"""
from src.core.text_processor import process_text_content
text = "Line 1\n\n\nLine 2"
result = process_text_content(text)
assert result == "Line 1 Line 2", \
f"FREE version must collapse multiple newlines, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'free')
def test_process_text_handles_mixed_whitespace_in_free(self):
"""FREE version should normalize mixed whitespace with newlines"""
from src.core.text_processor import process_text_content
text = "Line 1\n \nLine 2"
result = process_text_content(text)
# Should have no newlines and normalized spacing
assert "\n" not in result, \
f"FREE version must remove all newlines, got: {repr(result)}"
assert "Line 1" in result and "Line 2" in result, \
f"Must preserve actual text content, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'full')
def test_process_text_preserves_multiple_newlines_in_pro(self):
"""PRO version should preserve multiple consecutive newlines"""
from src.core.text_processor import process_text_content
text = "Line 1\n\n\nLine 2"
result = process_text_content(text)
assert result == "Line 1\n\n\nLine 2", \
f"PRO version must preserve all newlines, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'free')
def test_process_empty_text_in_free(self):
"""FREE version should handle empty text gracefully"""
from src.core.text_processor import process_text_content
result = process_text_content("")
assert result == "", \
f"Empty text should remain empty, got: {repr(result)}"
@patch('src.utils.constants.VERSION_TYPE', 'free')
def test_process_text_without_newlines_in_free(self):
"""FREE version should pass through text without newlines unchanged"""
from src.core.text_processor import process_text_content
text = "Single line text"
result = process_text_content(text)
assert result == "Single line text", \
f"Text without newlines should be unchanged, got: {repr(result)}"

View File

@@ -0,0 +1,181 @@
"""
Unit tests for modal text editor operator.
Tests verify:
- Operator class exists and is properly structured
- PRO-only access control via poll()
- Text processing and limit enforcement
- Proper Blender operator contract
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
class TestModalTextEditorOperator:
"""Test suite for TEXT_TEXTURE_OT_edit_multiline_text operator."""
def test_operator_class_exists(self):
"""Operator class must be importable and follow Blender conventions."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_idname')
assert TEXT_TEXTURE_OT_edit_multiline_text.bl_idname == "text_texture.edit_multiline_text"
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_label')
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'poll')
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'invoke')
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'execute')
assert hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'draw')
def test_poll_denies_access_in_free_version(self):
"""poll() must return False when has_multiline_text() is False (FREE version)."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
mock_context = Mock()
with patch('src.operators.text_editor_ops.has_multiline_text', return_value=False):
result = TEXT_TEXTURE_OT_edit_multiline_text.poll(mock_context)
assert result is False, "FREE version should not have access to multiline editor"
def test_poll_allows_access_in_pro_version(self):
"""poll() must return True when has_multiline_text() is True (PRO version)."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
mock_context = Mock()
with patch('src.operators.text_editor_ops.has_multiline_text', return_value=True):
result = TEXT_TEXTURE_OT_edit_multiline_text.poll(mock_context)
assert result is True, "PRO version should have access to multiline editor"
def test_invoke_copies_current_text_to_temp(self):
"""invoke() must copy scene text property to temp_text for editing."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
mock_context = Mock()
mock_context.scene.text_texture_props.text = "Line 1\nLine 2\nLine 3"
mock_context.window_manager.invoke_props_dialog.return_value = {'RUNNING_MODAL'}
mock_event = Mock()
result = operator.invoke(mock_context, mock_event)
assert operator.temp_text == "Line 1\nLine 2\nLine 3"
assert result == {'RUNNING_MODAL'}
mock_context.window_manager.invoke_props_dialog.assert_called_once()
def test_execute_saves_temp_text_back_to_property(self):
"""execute() must save edited temp_text back to scene property."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
operator.temp_text = "Edited Line 1\nEdited Line 2"
mock_context = Mock()
mock_context.scene.text_texture_props.text = "Old text"
with patch('src.operators.text_editor_ops.get_version_limits', return_value={
'max_text_lines': 50,
'max_text_length': 4096
}):
result = operator.execute(mock_context)
assert result == {'FINISHED'}
assert mock_context.scene.text_texture_props.text == "Edited Line 1\nEdited Line 2"
def test_execute_enforces_character_limit(self):
"""execute() must truncate text exceeding max_text_length."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
# Create text exceeding 100 char limit
operator.temp_text = "x" * 150
mock_context = Mock()
mock_context.scene.text_texture_props.text = ""
with patch('src.operators.text_editor_ops.get_version_limits', return_value={
'max_text_lines': 50,
'max_text_length': 100
}):
operator.execute(mock_context)
saved_text = mock_context.scene.text_texture_props.text
assert len(saved_text) == 100, f"Expected 100 chars, got {len(saved_text)}"
def test_execute_enforces_line_limit(self):
"""execute() must truncate lines exceeding max_text_lines."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
# Create 10 lines when limit is 5
operator.temp_text = "\n".join([f"Line {i}" for i in range(10)])
mock_context = Mock()
mock_context.scene.text_texture_props.text = ""
with patch('src.operators.text_editor_ops.get_version_limits', return_value={
'max_text_lines': 5,
'max_text_length': 4096
}):
operator.execute(mock_context)
saved_text = mock_context.scene.text_texture_props.text
line_count = saved_text.count('\n') + 1
assert line_count == 5, f"Expected 5 lines, got {line_count}"
assert saved_text == "Line 0\nLine 1\nLine 2\nLine 3\nLine 4"
def test_execute_handles_both_limits_simultaneously(self):
"""execute() must enforce both character and line limits correctly."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
# Create 10 lines of 20 chars each (200 chars total)
operator.temp_text = "\n".join([f"Line {i:015d}" for i in range(10)])
mock_context = Mock()
mock_context.scene.text_texture_props.text = ""
with patch('src.operators.text_editor_ops.get_version_limits', return_value={
'max_text_lines': 5,
'max_text_length': 150
}):
operator.execute(mock_context)
saved_text = mock_context.scene.text_texture_props.text
# Should truncate to 150 chars first, then check lines
assert len(saved_text) <= 150
line_count = saved_text.count('\n') + 1
assert line_count <= 5
def test_operator_has_temp_text_property(self):
"""Operator must have StringProperty named temp_text."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
operator = TEXT_TEXTURE_OT_edit_multiline_text()
# Blender properties are accessible as attributes
assert hasattr(operator, 'temp_text')
# Should be able to set and get
operator.temp_text = "Test content"
assert operator.temp_text == "Test content"
def test_operator_registration_list_exists(self):
"""Module must expose classes tuple for Blender registration."""
from src.operators import text_editor_ops
assert hasattr(text_editor_ops, 'classes')
assert isinstance(text_editor_ops.classes, tuple)
assert len(text_editor_ops.classes) > 0
def test_operator_module_has_register_function(self):
"""Module must have register() function for Blender."""
from src.operators import text_editor_ops
assert hasattr(text_editor_ops, 'register')
assert callable(text_editor_ops.register)
def test_operator_module_has_unregister_function(self):
"""Module must have unregister() function for Blender."""
from src.operators import text_editor_ops
assert hasattr(text_editor_ops, 'unregister')
assert callable(text_editor_ops.unregister)

View File

@@ -129,6 +129,26 @@ class TestVersionDetection:
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'text_fitting']):
assert has_text_fitting() is True
def test_has_multiline_text_returns_false_in_free_version(self):
"""Multiline text is not available in free version."""
from src.utils.constants import has_multiline_text
from unittest.mock import patch
# Test free version - need to patch ENABLED_FEATURES since it's computed at import
with patch('src.utils.constants.VERSION_TYPE', 'free'), \
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview']):
assert has_multiline_text() is False
def test_has_multiline_text_returns_true_in_pro_version(self):
"""Multiline text is available in PRO version."""
from src.utils.constants import has_multiline_text
from unittest.mock import patch
# Test full version - need to patch ENABLED_FEATURES since it's computed at import
with patch('src.utils.constants.VERSION_TYPE', 'full'), \
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'multiline_text']):
assert has_multiline_text() is True
class TestRuntimeImportDetection: