remove multiline
This commit is contained in:
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
@@ -59,7 +59,7 @@ The Text Texture Generator creates professional text textures directly in Blende
|
||||
|
||||
### 📖 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
|
||||
|
||||
@@ -282,7 +282,6 @@ Key Features:
|
||||
• Professional effects: shadows, glows, strokes, normal maps
|
||||
• Batch processing for multiple texts
|
||||
• 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
|
||||
• Cross-platform: Windows, macOS, Linux
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
## 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)
|
||||
|
||||
"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."
|
||||
|
||||
@@ -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!"
|
||||
|
||||
## 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)
|
||||
|
||||
"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."
|
||||
|
||||
@@ -33,7 +33,6 @@ from .text_fitting import (
|
||||
|
||||
# Text processing and rendering functions
|
||||
from .text_processor import (
|
||||
process_multiline_text,
|
||||
wrap_text_to_width,
|
||||
render_text_with_stroke,
|
||||
calculate_text_metrics,
|
||||
@@ -63,7 +62,6 @@ __all__ = [
|
||||
'resolve_text_conflicts',
|
||||
|
||||
# Text processing
|
||||
'process_multiline_text',
|
||||
'wrap_text_to_width',
|
||||
'render_text_with_stroke',
|
||||
'calculate_text_metrics',
|
||||
@@ -72,4 +70,4 @@ __all__ = [
|
||||
'rectangles_overlap',
|
||||
'find_non_overlapping_position',
|
||||
'validate_text_rendering'
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
pass
|
||||
"""Wrap text to fit within specified width"""
|
||||
@@ -379,19 +316,8 @@ def validate_text_rendering(img, text_positions):
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
from ..utils import constants
|
||||
|
||||
|
||||
def process_text_content(text: str) -> str:
|
||||
"""Process text content based on version capabilities.
|
||||
|
||||
FREE version: Strip newlines and normalize whitespace.
|
||||
PRO version: Preserve newlines for multiline rendering.
|
||||
"""
|
||||
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
|
||||
"""Normalize text content for single-line rendering."""
|
||||
text = text.replace('\n', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text
|
||||
|
||||
@@ -114,13 +114,6 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
||||
'normal_map_strength': props.normal_map_strength,
|
||||
'normal_map_blur_radius': props.normal_map_blur_radius,
|
||||
'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,
|
||||
'stroke_width': props.stroke_width,
|
||||
'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_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
|
||||
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
|
||||
props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
|
||||
@@ -830,4 +814,4 @@ __all__ = [
|
||||
'TEXT_TEXTURE_OT_refresh_presets',
|
||||
'TEXT_TEXTURE_OT_export_presets',
|
||||
'TEXT_TEXTURE_OT_import_presets',
|
||||
]
|
||||
]
|
||||
|
||||
@@ -208,12 +208,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
|
||||
)
|
||||
|
||||
# Collapsible section expand/collapse states
|
||||
expand_multiline: BoolProperty(
|
||||
name="Multiline Text",
|
||||
description="Show/hide Multiline Text section",
|
||||
default=False
|
||||
)
|
||||
|
||||
expand_image_overlays: BoolProperty(
|
||||
name="Image Overlays",
|
||||
description="Show/hide Image Overlays section",
|
||||
@@ -832,74 +826,6 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
|
||||
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
|
||||
# ============================================================================
|
||||
@@ -1077,4 +1003,4 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
|
||||
step=0.1,
|
||||
precision=1,
|
||||
update=update_live_preview
|
||||
)
|
||||
)
|
||||
|
||||
@@ -201,9 +201,6 @@ def draw_text_input_section(layout, props):
|
||||
"""Draw the main text input field"""
|
||||
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):
|
||||
pass
|
||||
"""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")
|
||||
|
||||
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):
|
||||
pass
|
||||
"""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="• Width 0 = no stroke")
|
||||
info_box.label(text="• Works with multiline text")
|
||||
|
||||
def draw_shadow_glow_section(layout, props):
|
||||
pass
|
||||
@@ -950,12 +905,6 @@ class TEXT_TEXTURE_PT_panel(Panel):
|
||||
|
||||
# === 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_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
||||
if font_section:
|
||||
@@ -1174,12 +1123,6 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
|
||||
|
||||
# === 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_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
||||
if font_section:
|
||||
@@ -1398,12 +1341,6 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
||||
|
||||
# === 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_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
||||
if font_section:
|
||||
@@ -1546,4 +1483,4 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
|
||||
|
||||
# Only show content if available and expanded
|
||||
if presets_expanded:
|
||||
draw_presets_section(presets_expanded, props)
|
||||
draw_presets_section(presets_expanded, props)
|
||||
|
||||
@@ -31,12 +31,13 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
|
||||
- Text that fits within constraints
|
||||
- Text requiring wrapping
|
||||
- Single word too long edge case
|
||||
- **TestProcessMultilineText** (4 tests)
|
||||
- **TestProcessTextContent** (5 tests)
|
||||
|
||||
- Single line processing
|
||||
- Multiline text handling
|
||||
- Height limit constraints
|
||||
- Empty text edge cases
|
||||
- Newline normalization
|
||||
- Multiple newline collapsing
|
||||
- Mixed whitespace handling
|
||||
- Empty string handling
|
||||
- No-newline passthrough
|
||||
|
||||
- **TestCalculateTextMetrics** (2 tests)
|
||||
|
||||
@@ -138,7 +139,7 @@ Comprehensive test suite for the Text Texture Generator Blender addon, focusing
|
||||
|
||||
### Core Algorithms
|
||||
|
||||
✅ Text wrapping and multiline processing
|
||||
✅ Text wrapping and newline normalization
|
||||
✅ Largest rectangle in histogram algorithm
|
||||
✅ Text positioning and alignment
|
||||
✅ Optimal font size calculation
|
||||
|
||||
@@ -144,7 +144,7 @@ def sample_text_long():
|
||||
|
||||
@pytest.fixture
|
||||
def sample_text_multiline():
|
||||
"""Multiline sample text for testing"""
|
||||
"""Sample text containing newlines for testing"""
|
||||
return "Line 1\nLine 2\nLine 3\nLine 4"
|
||||
|
||||
|
||||
@@ -259,4 +259,4 @@ def mock_props():
|
||||
props.normal_map_strength = 1.0
|
||||
props.normal_map_blur_radius = 0
|
||||
props.normal_map_invert = False
|
||||
return props
|
||||
return props
|
||||
|
||||
@@ -114,13 +114,6 @@ def addon_package(tmp_path, request):
|
||||
content = content.replace('VERSION_TYPE = "free"', f'VERSION_TYPE = "{version_type}"')
|
||||
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
|
||||
addon_zip = tmp_path / "text_texture_generator.zip"
|
||||
with zipfile.ZipFile(addon_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
@@ -129,4 +122,4 @@ def addon_package(tmp_path, request):
|
||||
arc_name = f"text_texture_generator/{file_path.relative_to(temp_src)}"
|
||||
zf.write(file_path, arc_name)
|
||||
|
||||
return addon_zip
|
||||
return addon_zip
|
||||
|
||||
@@ -800,56 +800,33 @@ print("SUCCESS: Single-line text remains unchanged")
|
||||
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"):
|
||||
- TEXT_TEXTURE_OT_edit_multiline_text.poll() returns False
|
||||
- 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)
|
||||
This verification runs inside Blender to ensure that stale helpers or
|
||||
operators are not exposed through the package API.
|
||||
"""
|
||||
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
|
||||
|
||||
# Verify multiline text feature is disabled
|
||||
from text_texture_generator.utils.constants import has_multiline_text
|
||||
|
||||
if has_multiline_text():
|
||||
print(f"FAIL: has_multiline_text() should return False in FREE version")
|
||||
def fail(msg):
|
||||
print(f"FAIL: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify operator is a MockOperator (text_editor_ops.py should be excluded)
|
||||
# has_multiline_text helper must be gone
|
||||
try:
|
||||
import text_texture_generator.operators.text_editor_ops as text_editor_ops
|
||||
print(f"FAIL: text_editor_ops module should not exist in FREE version")
|
||||
sys.exit(1)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected - module excluded from FREE build
|
||||
from text_texture_generator.utils.constants import has_multiline_text
|
||||
fail("has_multiline_text() helper is still importable")
|
||||
except ImportError:
|
||||
print("SUCCESS: has_multiline_text helper removed")
|
||||
|
||||
# Import operator from main module (should be MockOperator)
|
||||
import text_texture_generator
|
||||
op_class = text_texture_generator.TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
# Verify it's a MockOperator (no bl_rna, empty bl_idname)
|
||||
if hasattr(op_class, 'bl_rna'):
|
||||
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()")
|
||||
# Legacy multiline operator must also be absent
|
||||
try:
|
||||
from text_texture_generator import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
fail("TEXT_TEXTURE_OT_edit_multiline_text is still exposed")
|
||||
except ImportError:
|
||||
print("SUCCESS: Multiline editor operator removed")
|
||||
"""
|
||||
|
||||
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')
|
||||
|
||||
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
|
||||
@@ -516,286 +516,13 @@ except Exception as e:
|
||||
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,
|
||||
addon_package,
|
||||
tmp_path
|
||||
):
|
||||
"""
|
||||
Verify multiline editor operator poll() returns True in PRO version.
|
||||
|
||||
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
|
||||
Verify newline handling matches the single-line specification in PRO builds.
|
||||
"""
|
||||
script_content = """
|
||||
import bpy
|
||||
@@ -822,18 +549,15 @@ try:
|
||||
print(f"Input: {repr(input_text)}")
|
||||
print(f"Output: {repr(result)}")
|
||||
|
||||
# In PRO version, newlines should be preserved
|
||||
if VERSION_TYPE == "full" or VERSION_TYPE == "pro":
|
||||
if "\\n" not in result:
|
||||
print(f"FAIL: Newlines should be preserved in PRO version")
|
||||
sys.exit(1)
|
||||
|
||||
newline_count = result.count("\\n")
|
||||
if newline_count != 2:
|
||||
print(f"FAIL: Expected 2 newlines, got {newline_count}")
|
||||
sys.exit(1)
|
||||
if "\\n" in result:
|
||||
print("FAIL: Result should not contain newline characters")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Multiline text preserved in PRO version")
|
||||
if result != "Line 1 Line 2 Line 3":
|
||||
print(f"FAIL: Expected normalized output, got {repr(result)}")
|
||||
sys.exit(1)
|
||||
|
||||
print("SUCCESS: Text content normalized in PRO version")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
@@ -845,7 +569,7 @@ except Exception as e:
|
||||
# Copy test script
|
||||
tar_stream = io.BytesIO()
|
||||
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_info.size = len(script_bytes)
|
||||
tar.addfile(script_info, io.BytesIO(script_bytes))
|
||||
@@ -864,7 +588,7 @@ except Exception as e:
|
||||
addon_tar_stream.seek(0)
|
||||
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"
|
||||
|
||||
# Install addon
|
||||
@@ -887,7 +611,7 @@ except Exception as e:
|
||||
|
||||
assert install_result.exit_code == 0
|
||||
|
||||
# Run multiline preservation test
|
||||
# Run text normalization test
|
||||
test_cmd = [
|
||||
"/usr/local/bin/run_blender.sh",
|
||||
"--background",
|
||||
@@ -902,13 +626,13 @@ except Exception as e:
|
||||
|
||||
output = test_result.output.decode('utf-8', errors='replace')
|
||||
|
||||
print("\n=== Multiline Preservation Output ===")
|
||||
print("\n=== Text Normalization Output ===")
|
||||
print(output)
|
||||
print("=== End Output ===\n")
|
||||
|
||||
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 "SUCCESS: Multiline text preserved in PRO version" in output
|
||||
assert "SUCCESS: Text content normalized in PRO version" in output
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests how the core modules work together.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -18,17 +18,13 @@ class TestTextGenerationPipeline:
|
||||
"""Integration tests for the complete text generation workflow"""
|
||||
|
||||
@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.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"""
|
||||
# Setup mocks
|
||||
mock_process_text.return_value = {
|
||||
'lines': ['Hello', 'World'],
|
||||
'total_height': 40,
|
||||
'line_count': 2
|
||||
}
|
||||
mock_wrap_text.return_value = ['Hello', 'World']
|
||||
mock_find_size.return_value = 24
|
||||
mock_calc_position.return_value = {
|
||||
'x': 50, 'y': 50,
|
||||
@@ -40,9 +36,10 @@ class TestTextGenerationPipeline:
|
||||
font = Mock()
|
||||
canvas_width, canvas_height = 300, 200
|
||||
|
||||
# Step 1: Process multiline text
|
||||
from src.core.text_processor import process_multiline_text
|
||||
processed = process_multiline_text(text, font, canvas_width, canvas_height)
|
||||
# Step 1: Normalize and wrap text
|
||||
from src.core.text_processor import process_text_content, wrap_text_to_width
|
||||
normalized = process_text_content(text)
|
||||
wrapped_lines = wrap_text_to_width(normalized, font, canvas_width)
|
||||
|
||||
# Step 2: 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')
|
||||
|
||||
# Verify workflow completed
|
||||
assert processed['line_count'] == 2
|
||||
assert wrapped_lines == ['Hello', 'World']
|
||||
assert optimal_size == 24
|
||||
assert position['x'] == 50 and position['y'] == 50
|
||||
|
||||
# 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_calc_position.assert_called_once()
|
||||
|
||||
@@ -190,15 +187,13 @@ class TestErrorHandlingIntegration:
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_error_handling_in_text_processing_chain(self):
|
||||
"""Test error handling when text processing components fail"""
|
||||
with patch('src.core.text_processor.wrap_text_to_width') as mock_wrap:
|
||||
mock_wrap.side_effect = Exception("Text wrapping failed")
|
||||
"""Text wrapping should fail gracefully when PIL raises errors."""
|
||||
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.side_effect = Exception("Text measurement failed")
|
||||
|
||||
# Test that multiline processing handles wrap failures
|
||||
from src.core.text_processor import process_multiline_text
|
||||
result = process_multiline_text("Test text", Mock(), 200, 100)
|
||||
from src.core.text_processor import wrap_text_to_width
|
||||
result = wrap_text_to_width("Test text", Mock(), 200)
|
||||
|
||||
# Should return fallback result instead of crashing
|
||||
assert 'lines' in result
|
||||
assert 'total_height' in result
|
||||
assert 'line_count' in result
|
||||
assert result == ["Test text"]
|
||||
|
||||
@@ -1,180 +1,47 @@
|
||||
"""
|
||||
Unit tests for version-aware text processing behavior.
|
||||
Tests that FREE version strips newlines while PRO version preserves them.
|
||||
Unit tests for text content normalization.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
class TestProcessTextContent:
|
||||
"""Verify newline and whitespace handling for text processing."""
|
||||
|
||||
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"""
|
||||
def test_collapses_single_newlines(self):
|
||||
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)
|
||||
|
||||
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)}"
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
@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"""
|
||||
def test_collapses_multiple_newlines(self):
|
||||
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"""
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
def test_normalizes_mixed_whitespace(self):
|
||||
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)
|
||||
|
||||
# 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)}"
|
||||
assert "\n" not in result
|
||||
assert result == "Line 1 Line 2"
|
||||
|
||||
@patch('src.core.text_processor.VERSION_TYPE', 'free')
|
||||
def test_process_empty_text_in_free(self):
|
||||
"""FREE version should handle empty text gracefully"""
|
||||
def test_handles_empty_string(self):
|
||||
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"""
|
||||
assert result == ""
|
||||
|
||||
def test_preserves_text_without_newlines(self):
|
||||
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)}"
|
||||
assert result == text
|
||||
|
||||
@@ -28,22 +28,22 @@ class TestModalTextEditorOperator:
|
||||
assert hasattr(TEXT_TEXTURE_OT_edit_text, 'draw')
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
assert result is False, "FREE version should not have access to text editor"
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
assert result is True, "PRO version should have access to text editor"
|
||||
|
||||
@@ -125,4 +125,4 @@ class TestModalTextEditorOperator:
|
||||
from src.operators import text_editor_ops
|
||||
|
||||
assert hasattr(text_editor_ops, 'unregister')
|
||||
assert callable(text_editor_ops.unregister)
|
||||
assert callable(text_editor_ops.unregister)
|
||||
|
||||
@@ -9,28 +9,16 @@ src_path = Path(__file__).parent.parent.parent / "src"
|
||||
sys.path.insert(0, str(src_path))
|
||||
|
||||
|
||||
def test_has_multiline_text_function_complete():
|
||||
"""Test that has_multiline_text() is a complete function with body."""
|
||||
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_multiline_text_not_importable():
|
||||
"""Ensure has_multiline_text was fully removed."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'has_multiline_text'"):
|
||||
from utils.constants import has_multiline_text
|
||||
|
||||
|
||||
def test_has_text_editor_imports_from_constants():
|
||||
"""Test that has_text_editor can be imported from utils.constants."""
|
||||
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_has_text_editor_not_importable():
|
||||
"""Ensure has_text_editor helper was removed with the multiline feature."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'has_text_editor'"):
|
||||
from utils.constants import has_text_editor
|
||||
|
||||
|
||||
def test_new_operator_name_imports_from_operators():
|
||||
@@ -45,4 +33,4 @@ def test_new_operator_name_imports_from_operators():
|
||||
def test_old_operator_name_not_importable():
|
||||
"""Test that TEXT_TEXTURE_OT_edit_multiline_text (OLD name) is NOT importable."""
|
||||
with pytest.raises(ImportError, match="cannot import name 'TEXT_TEXTURE_OT_edit_multiline_text'"):
|
||||
from src.operators import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
from src.operators import TEXT_TEXTURE_OT_edit_multiline_text
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests the core text processing logic in isolation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -13,7 +13,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from fixtures.sample_data import *
|
||||
from src.core.text_processor import (
|
||||
process_multiline_text,
|
||||
wrap_text_to_width,
|
||||
render_text_with_stroke,
|
||||
calculate_text_metrics,
|
||||
@@ -97,68 +96,6 @@ class TestWrapTextToWidth:
|
||||
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:
|
||||
"""Test the calculate_text_metrics function"""
|
||||
@@ -416,4 +353,4 @@ class TestOptimizeTextLayout:
|
||||
"""Test optimization with empty text blocks list"""
|
||||
result = optimize_text_layout([], 500, 400)
|
||||
|
||||
assert result == []
|
||||
assert result == []
|
||||
|
||||
@@ -129,26 +129,6 @@ 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:
|
||||
@@ -317,4 +297,4 @@ class TestVersionDifferentiationScenarios:
|
||||
# Generous technical limits
|
||||
limits = get_version_limits()
|
||||
assert limits["max_texture_size"] == 4096, "Should have 4096px texture limit"
|
||||
assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit"
|
||||
assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit"
|
||||
|
||||
Reference in New Issue
Block a user