remove multiline

This commit is contained in:
2025-10-13 00:10:24 +02:00
parent 97950f44c1
commit ad55572aa8
21 changed files with 112 additions and 943 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -59,7 +59,7 @@ The Text Texture Generator creates professional text textures directly in Blende
### 📖 Medium Description (100 words) ### 📖 Medium Description (100 words)
Turn any text into beautiful, high-resolution textures with the Text Texture Generator Blender addon. Generate up to 4096×4096 textures with professional effects including drop shadows, glows, strokes, and normal maps. Features 9-point positioning, batch processing, and smart presets. No external software needed - everything happens inside Blender. Perfect for indie game developers, 3D artists, motion graphics creators, and anyone who needs custom text textures fast. Supports unlimited text, multiline layouts, and complete shader generation. Compatible with Blender 4.0+ on Windows, macOS, and Linux. Turn any text into beautiful, high-resolution textures with the Text Texture Generator Blender addon. Generate up to 4096×4096 textures with professional effects including drop shadows, glows, strokes, and normal maps. Features 9-point positioning, batch processing, and smart presets. No external software needed - everything happens inside Blender. Perfect for indie game developers, 3D artists, motion graphics creators, and anyone who needs custom text textures fast. Supports unlimited text and complete shader generation. Compatible with Blender 4.0+ on Windows, macOS, and Linux.
## Social Media Post Templates ## Social Media Post Templates
@@ -282,7 +282,6 @@ Key Features:
• Professional effects: shadows, glows, strokes, normal maps • Professional effects: shadows, glows, strokes, normal maps
• Batch processing for multiple texts • Batch processing for multiple texts
• Improved three-tier preset system (Blend File, Persistent, Local) with enhanced UI display • Improved three-tier preset system (Blend File, Persistent, Local) with enhanced UI display
• Multiline text support with auto-wrapping
• Complete shader generation with material assignment • Complete shader generation with material assignment
• Cross-platform: Windows, macOS, Linux • Cross-platform: Windows, macOS, Linux

View File

@@ -56,10 +56,6 @@ When I regenerate, notice it created a new material slot because the text conten
This preset can now be used in any other Blender file with one click. This is absolutely awesome if you're creating a product line or need consistent branding across multiple assets. No more recreating the same look over and over!" This preset can now be used in any other Blender file with one click. This is absolutely awesome if you're creating a product line or need consistent branding across multiple assets. No more recreating the same look over and over!"
## Multiline Text Support (4:10 - 4:25)
"The addon also handles multiline text beautifully. Watch as I add line breaks - the text automatically wraps at texture boundaries, maintaining proper spacing and alignment. Perfect for paragraphs or complex labels."
## Layout and Dimensions (4:25 - 4:45) ## Layout and Dimensions (4:25 - 4:45)
"Let's step into the layout options. Here's where you can change the dimensions of your texture - maybe you need a wide banner format or a square logo. Just adjust these values and regenerate." "Let's step into the layout options. Here's where you can change the dimensions of your texture - maybe you need a wide banner format or a square logo. Just adjust these values and regenerate."

View File

@@ -56,10 +56,6 @@ When I regenerate, notice it created a new material slot because the text conten
This preset can now be used in any other Blender file with one click. This is absolutely awesome if you're creating a product line or need consistent branding across multiple assets. No more recreating the same look over and over!" This preset can now be used in any other Blender file with one click. This is absolutely awesome if you're creating a product line or need consistent branding across multiple assets. No more recreating the same look over and over!"
## Multiline Text Support (4:10 - 4:25)
"The addon also handles multiline text beautifully. You can insert line breaks, and it automatically wraps at texture boundaries while maintaining proper spacing. Plus, you can adjust the text alignment—left, center, or right—so your multiline content is aligned exactly as you want."
## Layout and Dimensions (4:25 - 4:45) ## Layout and Dimensions (4:25 - 4:45)
"Let's step into the layout options. Here's where you can change the dimensions of your texture - maybe you need a wide banner format or a square logo. Just adjust these values and regenerate." "Let's step into the layout options. Here's where you can change the dimensions of your texture - maybe you need a wide banner format or a square logo. Just adjust these values and regenerate."

View File

@@ -33,7 +33,6 @@ from .text_fitting import (
# Text processing and rendering functions # Text processing and rendering functions
from .text_processor import ( from .text_processor import (
process_multiline_text,
wrap_text_to_width, wrap_text_to_width,
render_text_with_stroke, render_text_with_stroke,
calculate_text_metrics, calculate_text_metrics,
@@ -63,7 +62,6 @@ __all__ = [
'resolve_text_conflicts', 'resolve_text_conflicts',
# Text processing # Text processing
'process_multiline_text',
'wrap_text_to_width', 'wrap_text_to_width',
'render_text_with_stroke', 'render_text_with_stroke',
'calculate_text_metrics', 'calculate_text_metrics',

View File

@@ -1,66 +1,3 @@
def process_multiline_text(text, font, max_width, max_height):
pass
"""Process multiline text with automatic wrapping and fitting"""
try:
pass
from PIL import ImageFont, ImageDraw, Image
# Split text into lines
lines = text.split('\n')
processed_lines = []
# Create temporary image for measurements
temp_img = Image.new('RGBA', (1, 1))
draw = ImageDraw.Draw(temp_img)
total_height = 0
for line in lines:
pass
if not line.strip():
pass
# Empty line
bbox = draw.textbbox((0, 0), "A", font=font)
line_height = bbox[3] - bbox[1]
processed_lines.append("")
total_height += line_height
continue
# Wrap line if necessary
wrapped_lines = wrap_text_to_width(line, font, max_width)
for wrapped_line in wrapped_lines:
pass
bbox = draw.textbbox((0, 0), wrapped_line, font=font)
line_height = bbox[3] - bbox[1]
# Check if adding this line exceeds max height
if total_height + line_height > max_height and processed_lines:
pass
break
processed_lines.append(wrapped_line)
total_height += line_height
# Break if height limit reached
if total_height >= max_height:
pass
break
return {
'lines': processed_lines,
'total_height': total_height,
'line_count': len(processed_lines)
}
except Exception as e:
pass
import traceback
traceback.print_exc()
return {'lines': [text], 'total_height': 0, 'line_count': 1}
def wrap_text_to_width(text, font, max_width): def wrap_text_to_width(text, font, max_width):
pass pass
"""Wrap text to fit within specified width""" """Wrap text to fit within specified width"""
@@ -379,19 +316,8 @@ def validate_text_rendering(img, text_positions):
traceback.print_exc() traceback.print_exc()
return [] return []
from ..utils import constants
def process_text_content(text: str) -> str: def process_text_content(text: str) -> str:
"""Process text content based on version capabilities. """Normalize text content for single-line rendering."""
text = text.replace('\n', ' ')
FREE version: Strip newlines and normalize whitespace. text = ' '.join(text.split())
PRO version: Preserve newlines for multiline rendering. return text
"""
if constants.VERSION_TYPE == "free":
# Replace newlines with spaces and normalize consecutive whitespace
text = text.replace('\n', ' ')
text = ' '.join(text.split())
return text
else: # PRO version
return text

View File

@@ -114,13 +114,6 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
'normal_map_strength': props.normal_map_strength, 'normal_map_strength': props.normal_map_strength,
'normal_map_blur_radius': props.normal_map_blur_radius, 'normal_map_blur_radius': props.normal_map_blur_radius,
'normal_map_invert': props.normal_map_invert, 'normal_map_invert': props.normal_map_invert,
'enable_multiline': props.enable_multiline,
'line_height': props.line_height,
'text_alignment': props.text_alignment,
'auto_wrap': props.auto_wrap,
'wrap_width': props.wrap_width,
'max_lines': props.max_lines,
'line_spacing_pixels': props.line_spacing_pixels,
'enable_stroke': props.enable_stroke, 'enable_stroke': props.enable_stroke,
'stroke_width': props.stroke_width, 'stroke_width': props.stroke_width,
'stroke_position': props.stroke_position, 'stroke_position': props.stroke_position,
@@ -334,15 +327,6 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius) props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius)
props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert) props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert)
# Load multiline settings
props.enable_multiline = preset_data.get('enable_multiline', props.enable_multiline)
props.line_height = preset_data.get('line_height', props.line_height)
props.text_alignment = preset_data.get('text_alignment', props.text_alignment)
props.auto_wrap = preset_data.get('auto_wrap', props.auto_wrap)
props.wrap_width = preset_data.get('wrap_width', props.wrap_width)
props.max_lines = preset_data.get('max_lines', props.max_lines)
props.line_spacing_pixels = preset_data.get('line_spacing_pixels', props.line_spacing_pixels)
# Load stroke settings # Load stroke settings
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke) props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
props.stroke_width = preset_data.get('stroke_width', props.stroke_width) props.stroke_width = preset_data.get('stroke_width', props.stroke_width)

View File

@@ -208,12 +208,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
) )
# Collapsible section expand/collapse states # Collapsible section expand/collapse states
expand_multiline: BoolProperty(
name="Multiline Text",
description="Show/hide Multiline Text section",
default=False
)
expand_image_overlays: BoolProperty( expand_image_overlays: BoolProperty(
name="Image Overlays", name="Image Overlays",
description="Show/hide Image Overlays section", description="Show/hide Image Overlays section",
@@ -832,74 +826,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
update=update_live_preview update=update_live_preview
) )
# ============================================================================
# MULTILINE TEXT PROPERTIES
# ============================================================================
enable_multiline: BoolProperty(
name="Enable Multiline",
description="Enable multiline text rendering",
default=False,
update=update_live_preview
)
line_height: FloatProperty(
name="Line Height",
description="Line height multiplier (1.0 = normal, 1.5 = 150% spacing)",
default=1.2,
min=0.1,
max=5.0,
step=0.1,
precision=2,
update=update_live_preview
)
text_alignment: EnumProperty(
name="Text Alignment",
description="Text alignment for multiline text",
items=[
('LEFT', "Left", "Left align text"),
('CENTER', "Center", "Center align text"),
('RIGHT', "Right", "Right align text")
],
default='CENTER',
update=update_live_preview
)
auto_wrap: BoolProperty(
name="Auto Wrap",
description="Automatically wrap text to fit texture width",
default=True,
update=update_live_preview
)
wrap_width: IntProperty(
name="Wrap Width",
description="Maximum width in pixels before text wraps (0 = use texture width)",
default=0,
min=0,
max=10000,
update=update_live_preview
)
max_lines: IntProperty(
name="Max Lines",
description="Maximum number of lines (0 = unlimited)",
default=0,
min=0,
max=100,
update=update_live_preview
)
line_spacing_pixels: IntProperty(
name="Line Spacing (px)",
description="Additional spacing between lines in pixels",
default=0,
min=-50,
max=200,
update=update_live_preview
)
# ============================================================================ # ============================================================================
# STROKE SYSTEM PROPERTIES # STROKE SYSTEM PROPERTIES
# ============================================================================ # ============================================================================

View File

@@ -201,9 +201,6 @@ def draw_text_input_section(layout, props):
"""Draw the main text input field""" """Draw the main text input field"""
layout.prop(props, "text", text="") layout.prop(props, "text", text="")
# Note: Multiline text editor feature is not currently available
# The operator "text_texture.edit_text" does not exist in the codebase
def draw_font_dropdown_section(layout, props): def draw_font_dropdown_section(layout, props):
pass pass
"""Draw basic font dropdown for top level""" """Draw basic font dropdown for top level"""
@@ -706,47 +703,6 @@ def draw_text_fitting_section(layout, props):
info_box.label(text="• Respects min/max font size limits") info_box.label(text="• Respects min/max font size limits")
def draw_multiline_section(layout, props):
pass
"""Draw multiline text controls"""
# Main toggle for multiline text
layout.prop(props, "enable_multiline", text="Enable Multiline", icon='TEXT')
# Show controls only if multiline is enabled
if props.enable_multiline:
pass
layout.separator()
# Line height and alignment settings
col = layout.column(align=True)
col.prop(props, "line_height", text="Line Height", slider=True)
col.prop(props, "text_alignment", text="Alignment")
layout.separator()
# Auto wrap settings
layout.prop(props, "auto_wrap", text="Auto Wrap")
if props.auto_wrap:
pass
layout.prop(props, "wrap_width", text="Wrap Width (0 = texture width)")
layout.separator()
# Line limits and spacing
col = layout.column(align=True)
col.prop(props, "max_lines", text="Max Lines (0 = unlimited)")
col.prop(props, "line_spacing_pixels", text="Line Spacing (px)")
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Multiline text supports:", icon='INFO')
info_box.label(text="• Use \\n for manual line breaks")
info_box.label(text="• Auto-wrap fits text to width")
info_box.label(text="• Alignment works per line")
info_box.label(text="• Line height controls spacing")
def draw_stroke_section(layout, props): def draw_stroke_section(layout, props):
pass pass
"""Draw stroke and outline controls""" """Draw stroke and outline controls"""
@@ -789,7 +745,6 @@ def draw_stroke_section(layout, props):
info_box.label(text="• Center: Stroke on text edge") info_box.label(text="• Center: Stroke on text edge")
info_box.label(text="• Width 0 = no stroke") info_box.label(text="• Width 0 = no stroke")
info_box.label(text="• Works with multiline text")
def draw_shadow_glow_section(layout, props): def draw_shadow_glow_section(layout, props):
pass pass
@@ -950,12 +905,6 @@ class TEXT_TEXTURE_PT_panel(Panel):
# === FREE FEATURES === # === FREE FEATURES ===
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
pass
draw_multiline_section(multiline_section, props)
# Font Settings Section (collapsed by default) # Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section: if font_section:
@@ -1174,12 +1123,6 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
# === FREE FEATURES === # === FREE FEATURES ===
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
pass
draw_multiline_section(multiline_section, props)
# Font Settings Section (collapsed by default) # Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section: if font_section:
@@ -1398,12 +1341,6 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
# === FREE FEATURES === # === FREE FEATURES ===
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
pass
draw_multiline_section(multiline_section, props)
# Font Settings Section (collapsed by default) # Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section: if font_section:

View File

@@ -31,12 +31,13 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
- Text that fits within constraints - Text that fits within constraints
- Text requiring wrapping - Text requiring wrapping
- Single word too long edge case - Single word too long edge case
- **TestProcessMultilineText** (4 tests) - **TestProcessTextContent** (5 tests)
- Single line processing - Newline normalization
- Multiline text handling - Multiple newline collapsing
- Height limit constraints - Mixed whitespace handling
- Empty text edge cases - Empty string handling
- No-newline passthrough
- **TestCalculateTextMetrics** (2 tests) - **TestCalculateTextMetrics** (2 tests)
@@ -138,7 +139,7 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
### Core Algorithms ### Core Algorithms
✅ Text wrapping and multiline processing ✅ Text wrapping and newline normalization
✅ Largest rectangle in histogram algorithm ✅ Largest rectangle in histogram algorithm
✅ Text positioning and alignment ✅ Text positioning and alignment
✅ Optimal font size calculation ✅ Optimal font size calculation

View File

@@ -144,7 +144,7 @@ def sample_text_long():
@pytest.fixture @pytest.fixture
def sample_text_multiline(): def sample_text_multiline():
"""Multiline sample text for testing""" """Sample text containing newlines for testing"""
return "Line 1\nLine 2\nLine 3\nLine 4" return "Line 1\nLine 2\nLine 3\nLine 4"

View File

@@ -114,13 +114,6 @@ def addon_package(tmp_path, request):
content = content.replace('VERSION_TYPE = "free"', f'VERSION_TYPE = "{version_type}"') content = content.replace('VERSION_TYPE = "free"', f'VERSION_TYPE = "{version_type}"')
constants_file.write_text(content) constants_file.write_text(content)
# For FREE version, exclude PRO-only files
if version_type == "free":
# Remove text_editor_ops.py (multiline editor is PRO-only)
text_editor_ops = temp_src / "operators" / "text_editor_ops.py"
if text_editor_ops.exists():
text_editor_ops.unlink()
# Create zip from patched source # Create zip from patched source
addon_zip = tmp_path / "text_texture_generator.zip" addon_zip = tmp_path / "text_texture_generator.zip"
with zipfile.ZipFile(addon_zip, 'w', zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(addon_zip, 'w', zipfile.ZIP_DEFLATED) as zf:

View File

@@ -800,56 +800,33 @@ print("SUCCESS: Single-line text remains unchanged")
assert test_result.exit_code == 0, f"Single-line test failed:\n{output}" assert test_result.exit_code == 0, f"Single-line test failed:\n{output}"
def test_free_version_multiline_editor_operator_poll_blocks_access(blender_container, addon_package): def test_free_version_no_multiline_artifacts(blender_container, addon_package):
""" """
SPECIFICATION: FREE version must block multiline editor operator via poll(). SPECIFICATION: Multiline feature must be absent from the addon.
Expected behavior (VERSION_TYPE="free"): This verification runs inside Blender to ensure that stale helpers or
- TEXT_TEXTURE_OT_edit_multiline_text.poll() returns False operators are not exposed through the package API.
- Operator button should be disabled/grayed out in UI
- has_multiline_text() returns False
Current state (VERSION_TYPE="full"):
- Test SKIPS (multiline editor is available)
""" """
version_type, enabled_features = get_version_type(blender_container, addon_package)
if version_type == "full":
pytest.skip("Test requires VERSION_TYPE='free' - currently 'full' (specification test)")
script_content = """ script_content = """
import bpy
import sys import sys
# Verify multiline text feature is disabled def fail(msg):
from text_texture_generator.utils.constants import has_multiline_text print(f"FAIL: {msg}")
if has_multiline_text():
print(f"FAIL: has_multiline_text() should return False in FREE version")
sys.exit(1) sys.exit(1)
# Verify operator is a MockOperator (text_editor_ops.py should be excluded) # has_multiline_text helper must be gone
try: try:
import text_texture_generator.operators.text_editor_ops as text_editor_ops from text_texture_generator.utils.constants import has_multiline_text
print(f"FAIL: text_editor_ops module should not exist in FREE version") fail("has_multiline_text() helper is still importable")
sys.exit(1) except ImportError:
except (ImportError, ModuleNotFoundError): print("SUCCESS: has_multiline_text helper removed")
pass # Expected - module excluded from FREE build
# Import operator from main module (should be MockOperator) # Legacy multiline operator must also be absent
import text_texture_generator try:
op_class = text_texture_generator.TEXT_TEXTURE_OT_edit_multiline_text from text_texture_generator import TEXT_TEXTURE_OT_edit_multiline_text
fail("TEXT_TEXTURE_OT_edit_multiline_text is still exposed")
# Verify it's a MockOperator (no bl_rna, empty bl_idname) except ImportError:
if hasattr(op_class, 'bl_rna'): print("SUCCESS: Multiline editor operator removed")
print(f"FAIL: Operator should be MockOperator (has bl_rna)")
sys.exit(1)
if not hasattr(op_class, 'bl_idname') or op_class.bl_idname != "":
print(f"FAIL: MockOperator should have empty bl_idname, got: {getattr(op_class, 'bl_idname', 'MISSING')}")
sys.exit(1)
print("SUCCESS: Multiline editor operator correctly blocked via poll()")
""" """
tar_stream = io.BytesIO() tar_stream = io.BytesIO()
@@ -873,61 +850,6 @@ print("SUCCESS: Multiline editor operator correctly blocked via poll()")
output = test_result.output.decode('utf-8', errors='replace') output = test_result.output.decode('utf-8', errors='replace')
assert test_result.exit_code == 0, f"Operator poll test failed:\n{output}" assert test_result.exit_code == 0, f"Operator poll test failed:\n{output}"
assert "SUCCESS: Multiline editor operator correctly blocked" in output assert "SUCCESS: has_multiline_text helper removed" in output
assert "SUCCESS: Multiline editor operator removed" in output
def test_free_version_multiline_editor_invoke_fails_gracefully(blender_container, addon_package):
"""
SPECIFICATION: FREE version must fail gracefully if operator is invoked.
Expected behavior (VERSION_TYPE="free"):
- Even if poll() is bypassed, invoke should fail gracefully
- No crashes or exceptions
- Returns appropriate error status
Current state (VERSION_TYPE="full"):
- Test SKIPS (operator works normally)
"""
version_type, enabled_features = get_version_type(blender_container, addon_package)
if version_type == "full":
pytest.skip("Test requires VERSION_TYPE='free' - currently 'full' (specification test)")
script_content = """
import bpy
import sys
# Attempt to import the operator - in FREE version, this module should not exist
try:
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
# If we get here in FREE version, something is wrong
print("ERROR: text_editor_ops should not exist in FREE version")
sys.exit(1)
except ImportError:
# Expected in FREE version - module should not exist
print("SUCCESS: Multiline editor module correctly absent in FREE version")
sys.exit(0)
"""
tar_stream = io.BytesIO()
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
script_info = tarfile.TarInfo(name='test_editor_invoke.py')
script_bytes = script_content.encode('utf-8')
script_info.size = len(script_bytes)
tar.addfile(script_info, io.BytesIO(script_bytes))
tar_stream.seek(0)
blender_container.put_archive('/addon-test', tar_stream)
test_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python",
"/addon-test/test_editor_invoke.py"
]
test_result = blender_container.exec_run(test_cmd, workdir="/addon-test")
output = test_result.output.decode('utf-8', errors='replace')
assert test_result.exit_code == 0, f"Operator invoke test failed:\n{output}"
assert "SUCCESS: Multiline editor module correctly absent in FREE version" in output

View File

@@ -516,286 +516,13 @@ except Exception as e:
assert "Can set 4096px: True" in output assert "Can set 4096px: True" in output
def test_pro_version_multiline_editor_operator_poll_allows_access( def test_pro_version_text_normalization_matches_free_version(
blender_container, blender_container,
addon_package, addon_package,
tmp_path tmp_path
): ):
""" """
Verify multiline editor operator poll() returns True in PRO version. Verify newline handling matches the single-line specification in PRO builds.
Expected behavior (VERSION_TYPE="full"):
- TEXT_TEXTURE_OT_edit_multiline_text.poll() returns True
- Operator button should be enabled in UI
- has_multiline_text() returns True
"""
script_content = """
import bpy
import sys
# Enable addon
try:
bpy.ops.preferences.addon_enable(module='text_texture_generator')
except Exception as e:
print(f"ERROR enabling addon: {e}")
sys.exit(1)
# Verify multiline text feature is enabled
try:
from text_texture_generator.utils.constants import has_multiline_text, VERSION_TYPE
print(f"VERSION_TYPE: {VERSION_TYPE}")
if not has_multiline_text():
print(f"FAIL: has_multiline_text() should return True in PRO version")
sys.exit(1)
# Verify operator poll returns True
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
# Create a minimal mock context
class MockContext:
pass
context = MockContext()
poll_result = TEXT_TEXTURE_OT_edit_multiline_text.poll(context)
if not poll_result:
print(f"FAIL: Operator poll() should return True in PRO version")
sys.exit(1)
print("SUCCESS: Multiline editor operator poll() returns True")
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
"""
# Copy test script
tar_stream = io.BytesIO()
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
script_info = tarfile.TarInfo(name='test_editor_poll.py')
script_bytes = script_content.encode('utf-8')
script_info.size = len(script_bytes)
tar.addfile(script_info, io.BytesIO(script_bytes))
tar_stream.seek(0)
blender_container.put_archive('/addon-test', tar_stream)
# Copy addon package
addon_tar_stream = io.BytesIO()
with tarfile.open(fileobj=addon_tar_stream, mode='w') as tar:
addon_info = tarfile.TarInfo(name='text_texture_generator.zip')
addon_info.size = addon_package.stat().st_size
with open(addon_package, 'rb') as f:
tar.addfile(addon_info, f)
addon_tar_stream.seek(0)
blender_container.put_archive('/addon-test', addon_tar_stream)
container_script_path = "/addon-test/test_editor_poll.py"
container_addon_path = "/addon-test/text_texture_generator.zip"
# Install addon
install_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python-expr",
(
"import bpy; "
f"bpy.ops.preferences.addon_install(filepath='{container_addon_path}'); "
"bpy.ops.preferences.addon_enable(module='text_texture_generator'); "
"bpy.ops.wm.save_userpref()"
)
]
install_result = blender_container.exec_run(
install_cmd,
workdir="/addon-test"
)
assert install_result.exit_code == 0
# Run poll verification test
test_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python",
container_script_path
]
test_result = blender_container.exec_run(
test_cmd,
workdir="/addon-test"
)
output = test_result.output.decode('utf-8', errors='replace')
print("\n=== Multiline Editor Poll Output ===")
print(output)
print("=== End Output ===\n")
assert test_result.exit_code == 0, (
f"Poll verification failed with exit code {test_result.exit_code}\n{output}"
)
assert "VERSION_TYPE: full" in output
assert "SUCCESS: Multiline editor operator poll() returns True" in output
def test_pro_version_multiline_editor_can_be_invoked(
blender_container,
addon_package,
tmp_path
):
"""
Verify multiline editor operator can be successfully invoked in PRO version.
Expected behavior (VERSION_TYPE="full"):
- Operator can be invoked without errors
- Dialog is opened (invoke_props_dialog called)
- temp_text is initialized from current text property
"""
script_content = """
import bpy
import sys
# Enable addon
try:
bpy.ops.preferences.addon_enable(module='text_texture_generator')
except Exception as e:
print(f"ERROR enabling addon: {e}")
sys.exit(1)
# Test operator invocation
try:
from text_texture_generator.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text
# Create scene and properties
bpy.ops.mesh.primitive_cube_add()
context = bpy.context
# Verify operator is registered and accessible
# We can't directly instantiate Blender operators, but we can verify registration
if not hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_idname'):
print("ERROR: Operator missing bl_idname")
sys.exit(1)
# Verify it's a real Blender operator (has bl_rna)
if not hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'bl_rna'):
print("ERROR: Operator is not a proper Blender operator (missing bl_rna)")
sys.exit(1)
# Verify temp_text property is defined (check annotations or __annotations__)
has_temp_text = (
hasattr(TEXT_TEXTURE_OT_edit_multiline_text, 'temp_text') or
(hasattr(TEXT_TEXTURE_OT_edit_multiline_text, '__annotations__') and
'temp_text' in TEXT_TEXTURE_OT_edit_multiline_text.__annotations__)
)
if not has_temp_text:
print("ERROR: Operator missing temp_text property")
print(f"Available attributes: {dir(TEXT_TEXTURE_OT_edit_multiline_text)}")
sys.exit(1)
print("Operator registered successfully")
print("temp_text property exists: True")
print("SUCCESS: Multiline editor operator can be invoked")
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
"""
# Copy test script
tar_stream = io.BytesIO()
with tarfile.open(fileobj=tar_stream, mode='w') as tar:
script_info = tarfile.TarInfo(name='test_editor_invoke.py')
script_bytes = script_content.encode('utf-8')
script_info.size = len(script_bytes)
tar.addfile(script_info, io.BytesIO(script_bytes))
tar_stream.seek(0)
blender_container.put_archive('/addon-test', tar_stream)
# Copy addon package
addon_tar_stream = io.BytesIO()
with tarfile.open(fileobj=addon_tar_stream, mode='w') as tar:
addon_info = tarfile.TarInfo(name='text_texture_generator.zip')
addon_info.size = addon_package.stat().st_size
with open(addon_package, 'rb') as f:
tar.addfile(addon_info, f)
addon_tar_stream.seek(0)
blender_container.put_archive('/addon-test', addon_tar_stream)
container_script_path = "/addon-test/test_editor_invoke.py"
container_addon_path = "/addon-test/text_texture_generator.zip"
# Install addon
install_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python-expr",
(
"import bpy; "
f"bpy.ops.preferences.addon_install(filepath='{container_addon_path}'); "
"bpy.ops.preferences.addon_enable(module='text_texture_generator'); "
"bpy.ops.wm.save_userpref()"
)
]
install_result = blender_container.exec_run(
install_cmd,
workdir="/addon-test"
)
assert install_result.exit_code == 0
# Run invoke test
test_cmd = [
"/usr/local/bin/run_blender.sh",
"--background",
"--python",
container_script_path
]
test_result = blender_container.exec_run(
test_cmd,
workdir="/addon-test"
)
output = test_result.output.decode('utf-8', errors='replace')
print("\n=== Multiline Editor Invoke Output ===")
print(output)
print("=== End Output ===\n")
assert test_result.exit_code == 0, (
f"Invoke test failed with exit code {test_result.exit_code}\n{output}"
)
assert "Operator registered successfully" in output
assert "temp_text property exists: True" in output
assert "SUCCESS: Multiline editor operator can be invoked" in output
def test_pro_version_multiline_text_preserved_during_processing(
blender_container,
addon_package,
tmp_path
):
"""
Verify multiline text with newlines is preserved in PRO version.
Expected behavior (VERSION_TYPE="full"):
- Text processor preserves \\n characters in PRO version
- "Line 1\\nLine 2\\nLine 3" remains unchanged
- No newline stripping in PRO mode
""" """
script_content = """ script_content = """
import bpy import bpy
@@ -822,18 +549,15 @@ try:
print(f"Input: {repr(input_text)}") print(f"Input: {repr(input_text)}")
print(f"Output: {repr(result)}") print(f"Output: {repr(result)}")
# In PRO version, newlines should be preserved if "\\n" in result:
if VERSION_TYPE == "full" or VERSION_TYPE == "pro": print("FAIL: Result should not contain newline characters")
if "\\n" not in result: sys.exit(1)
print(f"FAIL: Newlines should be preserved in PRO version")
sys.exit(1)
newline_count = result.count("\\n") if result != "Line 1 Line 2 Line 3":
if newline_count != 2: print(f"FAIL: Expected normalized output, got {repr(result)}")
print(f"FAIL: Expected 2 newlines, got {newline_count}") sys.exit(1)
sys.exit(1)
print("SUCCESS: Multiline text preserved in PRO version") print("SUCCESS: Text content normalized in PRO version")
except Exception as e: except Exception as e:
print(f"ERROR: {e}") print(f"ERROR: {e}")
@@ -845,7 +569,7 @@ except Exception as e:
# Copy test script # Copy test script
tar_stream = io.BytesIO() tar_stream = io.BytesIO()
with tarfile.open(fileobj=tar_stream, mode='w') as tar: with tarfile.open(fileobj=tar_stream, mode='w') as tar:
script_info = tarfile.TarInfo(name='test_multiline_preserve.py') script_info = tarfile.TarInfo(name='test_text_normalization.py')
script_bytes = script_content.encode('utf-8') script_bytes = script_content.encode('utf-8')
script_info.size = len(script_bytes) script_info.size = len(script_bytes)
tar.addfile(script_info, io.BytesIO(script_bytes)) tar.addfile(script_info, io.BytesIO(script_bytes))
@@ -864,7 +588,7 @@ except Exception as e:
addon_tar_stream.seek(0) addon_tar_stream.seek(0)
blender_container.put_archive('/addon-test', addon_tar_stream) blender_container.put_archive('/addon-test', addon_tar_stream)
container_script_path = "/addon-test/test_multiline_preserve.py" container_script_path = "/addon-test/test_text_normalization.py"
container_addon_path = "/addon-test/text_texture_generator.zip" container_addon_path = "/addon-test/text_texture_generator.zip"
# Install addon # Install addon
@@ -887,7 +611,7 @@ except Exception as e:
assert install_result.exit_code == 0 assert install_result.exit_code == 0
# Run multiline preservation test # Run text normalization test
test_cmd = [ test_cmd = [
"/usr/local/bin/run_blender.sh", "/usr/local/bin/run_blender.sh",
"--background", "--background",
@@ -902,13 +626,13 @@ except Exception as e:
output = test_result.output.decode('utf-8', errors='replace') output = test_result.output.decode('utf-8', errors='replace')
print("\n=== Multiline Preservation Output ===") print("\n=== Text Normalization Output ===")
print(output) print(output)
print("=== End Output ===\n") print("=== End Output ===\n")
assert test_result.exit_code == 0, ( assert test_result.exit_code == 0, (
f"Multiline preservation test failed with exit code {test_result.exit_code}\n{output}" f"Text normalization test failed with exit code {test_result.exit_code}\n{output}"
) )
assert "VERSION_TYPE: full" in output assert "VERSION_TYPE: full" in output
assert "SUCCESS: Multiline text preserved in PRO version" in output assert "SUCCESS: Text content normalized in PRO version" in output

View File

@@ -4,7 +4,7 @@ Tests how the core modules work together.
""" """
import pytest import pytest
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch
import sys import sys
import os import os
@@ -18,17 +18,13 @@ class TestTextGenerationPipeline:
"""Integration tests for the complete text generation workflow""" """Integration tests for the complete text generation workflow"""
@pytest.mark.integration @pytest.mark.integration
@patch('src.core.text_processor.process_multiline_text') @patch('src.core.text_processor.wrap_text_to_width')
@patch('src.core.text_fitting.find_optimal_font_size') @patch('src.core.text_fitting.find_optimal_font_size')
@patch('src.core.text_fitting.calculate_text_position') @patch('src.core.text_fitting.calculate_text_position')
def test_basic_text_generation_workflow(self, mock_calc_position, mock_find_size, mock_process_text): def test_basic_text_generation_workflow(self, mock_calc_position, mock_find_size, mock_wrap_text):
"""Test the basic workflow from text input to positioned output""" """Test the basic workflow from text input to positioned output"""
# Setup mocks # Setup mocks
mock_process_text.return_value = { mock_wrap_text.return_value = ['Hello', 'World']
'lines': ['Hello', 'World'],
'total_height': 40,
'line_count': 2
}
mock_find_size.return_value = 24 mock_find_size.return_value = 24
mock_calc_position.return_value = { mock_calc_position.return_value = {
'x': 50, 'y': 50, 'x': 50, 'y': 50,
@@ -40,9 +36,10 @@ class TestTextGenerationPipeline:
font = Mock() font = Mock()
canvas_width, canvas_height = 300, 200 canvas_width, canvas_height = 300, 200
# Step 1: Process multiline text # Step 1: Normalize and wrap text
from src.core.text_processor import process_multiline_text from src.core.text_processor import process_text_content, wrap_text_to_width
processed = process_multiline_text(text, font, canvas_width, canvas_height) normalized = process_text_content(text)
wrapped_lines = wrap_text_to_width(normalized, font, canvas_width)
# Step 2: Find optimal font size # Step 2: Find optimal font size
from src.core.text_fitting import find_optimal_font_size from src.core.text_fitting import find_optimal_font_size
@@ -54,12 +51,12 @@ class TestTextGenerationPipeline:
position = calculate_text_position(text, font, available_area, 'center') position = calculate_text_position(text, font, available_area, 'center')
# Verify workflow completed # Verify workflow completed
assert processed['line_count'] == 2 assert wrapped_lines == ['Hello', 'World']
assert optimal_size == 24 assert optimal_size == 24
assert position['x'] == 50 and position['y'] == 50 assert position['x'] == 50 and position['y'] == 50
# Verify functions were called in sequence # Verify functions were called in sequence
mock_process_text.assert_called_once() mock_wrap_text.assert_called_once_with(normalized, font, canvas_width)
mock_find_size.assert_called_once() mock_find_size.assert_called_once()
mock_calc_position.assert_called_once() mock_calc_position.assert_called_once()
@@ -190,15 +187,13 @@ class TestErrorHandlingIntegration:
@pytest.mark.integration @pytest.mark.integration
def test_error_handling_in_text_processing_chain(self): def test_error_handling_in_text_processing_chain(self):
"""Test error handling when text processing components fail""" """Text wrapping should fail gracefully when PIL raises errors."""
with patch('src.core.text_processor.wrap_text_to_width') as mock_wrap: with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
mock_wrap.side_effect = Exception("Text wrapping failed") mock_draw = Mock()
mock_draw_class.return_value = mock_draw
mock_draw.textbbox.side_effect = Exception("Text measurement failed")
# Test that multiline processing handles wrap failures from src.core.text_processor import wrap_text_to_width
from src.core.text_processor import process_multiline_text result = wrap_text_to_width("Test text", Mock(), 200)
result = process_multiline_text("Test text", Mock(), 200, 100)
# Should return fallback result instead of crashing assert result == ["Test text"]
assert 'lines' in result
assert 'total_height' in result
assert 'line_count' in result

View File

@@ -1,180 +1,47 @@
""" """
Unit tests for version-aware text processing behavior. Unit tests for text content normalization.
Tests that FREE version strips newlines while PRO version preserves them.
""" """
import pytest
from unittest.mock import patch
class TestProcessTextContent:
"""Verify newline and whitespace handling for text processing."""
class TestVersionAwareNewlineHandling: def test_collapses_single_newlines(self):
"""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 from src.core.text_processor import process_text_content
text = "Line 1\nLine 2\nLine 3" text = "Line 1\nLine 2"
result = process_text_content(text) result = process_text_content(text)
assert result == "Line 1 Line 2 Line 3", \ assert result == "Line 1 Line 2"
f"FREE version must strip newlines, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'full') def test_collapses_multiple_newlines(self):
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 from src.core.text_processor import process_text_content
text = "Line 1\n\n\nLine 2" text = "Line 1\n\n\nLine 2"
result = process_text_content(text) result = process_text_content(text)
assert result == "Line 1 Line 2", \ 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_normalizes_mixed_whitespace(self):
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 from src.core.text_processor import process_text_content
text = "Line 1\n \nLine 2" text = "Line 1\n \t\nLine 2"
result = process_text_content(text) result = process_text_content(text)
# Should have no newlines and normalized spacing assert "\n" not in result
assert "\n" not in result, \ assert result == "Line 1 Line 2"
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_handles_empty_string(self):
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 from src.core.text_processor import process_text_content
result = process_text_content("") result = process_text_content("")
assert result == "", \ assert result == ""
f"Empty text should remain empty, got: {repr(result)}"
@patch('src.core.text_processor.VERSION_TYPE', 'free') def test_preserves_text_without_newlines(self):
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 from src.core.text_processor import process_text_content
text = "Single line text" text = "Single line text"
result = process_text_content(text) result = process_text_content(text)
assert result == "Single line text", \ assert result == 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

@@ -28,22 +28,22 @@ class TestModalTextEditorOperator:
assert hasattr(TEXT_TEXTURE_OT_edit_text, 'draw') assert hasattr(TEXT_TEXTURE_OT_edit_text, 'draw')
def test_poll_denies_access_in_free_version(self): def test_poll_denies_access_in_free_version(self):
"""poll() must return False when has_text_editor() is False (FREE version).""" """poll() must return False when running in the free version."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text
mock_context = Mock() mock_context = Mock()
with patch('src.operators.text_editor_ops.has_text_editor', return_value=False): with patch('src.operators.text_editor_ops.is_full_version', return_value=False):
result = TEXT_TEXTURE_OT_edit_text.poll(mock_context) result = TEXT_TEXTURE_OT_edit_text.poll(mock_context)
assert result is False, "FREE version should not have access to text editor" assert result is False, "FREE version should not have access to text editor"
def test_poll_allows_access_in_pro_version(self): def test_poll_allows_access_in_pro_version(self):
"""poll() must return True when has_text_editor() is True (PRO version).""" """poll() must return True when running in the pro version."""
from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_text
mock_context = Mock() mock_context = Mock()
with patch('src.operators.text_editor_ops.has_text_editor', return_value=True): with patch('src.operators.text_editor_ops.is_full_version', return_value=True):
result = TEXT_TEXTURE_OT_edit_text.poll(mock_context) result = TEXT_TEXTURE_OT_edit_text.poll(mock_context)
assert result is True, "PRO version should have access to text editor" assert result is True, "PRO version should have access to text editor"

View File

@@ -9,28 +9,16 @@ src_path = Path(__file__).parent.parent.parent / "src"
sys.path.insert(0, str(src_path)) sys.path.insert(0, str(src_path))
def test_has_multiline_text_function_complete(): def test_has_multiline_text_not_importable():
"""Test that has_multiline_text() is a complete function with body.""" """Ensure has_multiline_text was fully removed."""
from utils.constants import has_multiline_text with pytest.raises(ImportError, match="cannot import name 'has_multiline_text'"):
from utils.constants import has_multiline_text
# Should be callable
assert callable(has_multiline_text)
# Should return a boolean (not crash with incomplete body)
result = has_multiline_text()
assert isinstance(result, bool)
def test_has_text_editor_imports_from_constants(): def test_has_text_editor_not_importable():
"""Test that has_text_editor can be imported from utils.constants.""" """Ensure has_text_editor helper was removed with the multiline feature."""
from utils.constants import has_text_editor with pytest.raises(ImportError, match="cannot import name 'has_text_editor'"):
from utils.constants import has_text_editor
# Verify it's callable (should be a function)
assert callable(has_text_editor)
# Should return a boolean without crashing
result = has_text_editor()
assert isinstance(result, bool)
def test_new_operator_name_imports_from_operators(): def test_new_operator_name_imports_from_operators():

View File

@@ -4,7 +4,7 @@ Tests the core text processing logic in isolation.
""" """
import pytest import pytest
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch
import sys import sys
import os import os
@@ -13,7 +13,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
from fixtures.sample_data import * from fixtures.sample_data import *
from src.core.text_processor import ( from src.core.text_processor import (
process_multiline_text,
wrap_text_to_width, wrap_text_to_width,
render_text_with_stroke, render_text_with_stroke,
calculate_text_metrics, calculate_text_metrics,
@@ -97,68 +96,6 @@ class TestWrapTextToWidth:
assert "SuperLongWordThatCannotFit" in result assert "SuperLongWordThatCannotFit" in result
class TestProcessMultilineText:
"""Test the process_multiline_text function"""
@pytest.mark.unit
@patch('src.core.text_processor.wrap_text_to_width')
def test_process_single_line_text(self, mock_wrap, sample_font):
"""Test processing single line text"""
mock_wrap.return_value = [SHORT_TEXT]
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
mock_draw.textbbox.return_value = (0, 0, 100, 20)
result = process_multiline_text(SHORT_TEXT, sample_font, 200, 100)
assert result['line_count'] == 1
assert result['lines'] == [SHORT_TEXT]
assert result['total_height'] == 20
@pytest.mark.unit
@patch('src.core.text_processor.wrap_text_to_width')
def test_process_multiline_text_basic(self, mock_wrap, sample_font):
"""Test processing actual multiline text"""
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
mock_draw.textbbox.return_value = (0, 0, 80, 20)
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 200)
assert result['line_count'] == 4
assert len(result['lines']) == 4
assert result['total_height'] == 80 # 4 lines * 20 height
@pytest.mark.unit
def test_process_empty_text(self, sample_font):
"""Test processing empty text"""
result = process_multiline_text("", sample_font, 200, 100)
assert result['line_count'] == 1
assert result['lines'] == [""]
@pytest.mark.unit
@patch('src.core.text_processor.wrap_text_to_width')
def test_process_height_limit_exceeded(self, mock_wrap, sample_font):
"""Test text processing when height limit is exceeded"""
mock_wrap.side_effect = [['Line 1'], ['Line 2'], ['Line 3'], ['Line 4']]
with patch('PIL.Image.new'), patch('PIL.ImageDraw.Draw') as mock_draw_class:
mock_draw = Mock()
mock_draw_class.return_value = mock_draw
mock_draw.textbbox.return_value = (0, 0, 80, 30) # Each line is 30px high
# With max_height=50, only 1 line should fit
result = process_multiline_text(MULTILINE_TEXT, sample_font, 200, 50)
assert result['line_count'] <= 2 # Should stop when height limit reached
assert result['total_height'] <= 60 # Should not exceed much
class TestCalculateTextMetrics: class TestCalculateTextMetrics:
"""Test the calculate_text_metrics function""" """Test the calculate_text_metrics function"""

View File

@@ -129,26 +129,6 @@ class TestVersionDetection:
patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'text_fitting']): patch('src.utils.constants.ENABLED_FEATURES', ['basic', 'preview', 'text_fitting']):
assert has_text_fitting() is True 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: class TestRuntimeImportDetection: