316 lines
11 KiB
Python
316 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Standalone property-based test for preview generation.
|
|
|
|
This test verifies that preview generation works correctly by testing
|
|
the certificate generation logic with sample data.
|
|
|
|
Feature: survey-custom-certificate-template, Property 9: Preview generation
|
|
Validates: Requirements 4.2
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import io
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
try:
|
|
from hypothesis import given, settings, HealthCheck
|
|
HYPOTHESIS_AVAILABLE = True
|
|
except ImportError:
|
|
print("ERROR: Hypothesis is not installed. Install with: pip install hypothesis")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
from docx import Document
|
|
DOCX_AVAILABLE = True
|
|
except ImportError:
|
|
print("ERROR: python-docx is not installed. Install with: pip install python-docx")
|
|
sys.exit(1)
|
|
|
|
# Import our custom strategies
|
|
from hypothesis_strategies import docx_with_mappings_and_data
|
|
|
|
|
|
def generate_sample_data():
|
|
"""
|
|
Generate sample data for preview (mimics wizard's _generate_sample_data).
|
|
"""
|
|
from datetime import datetime
|
|
|
|
return {
|
|
'survey_title': 'Sample Course Title',
|
|
'survey_description': 'Sample course description',
|
|
'partner_name': 'John Doe',
|
|
'partner_email': 'john.doe@example.com',
|
|
'email': 'john.doe@example.com',
|
|
'create_date': datetime.now().strftime('%Y-%m-%d'),
|
|
'completion_date': datetime.now().strftime('%Y-%m-%d'),
|
|
'scoring_percentage': '95.5',
|
|
'scoring_total': '100',
|
|
}
|
|
|
|
|
|
def replace_placeholders_in_docx(docx_binary, mappings, data):
|
|
"""
|
|
Replace placeholders in a DOCX document with data.
|
|
|
|
This simulates the certificate generator's placeholder replacement logic.
|
|
"""
|
|
# Load the document
|
|
doc_stream = io.BytesIO(docx_binary)
|
|
doc = Document(doc_stream)
|
|
|
|
# Build a replacement dictionary
|
|
replacements = {}
|
|
for mapping in mappings['placeholders']:
|
|
key = mapping['key']
|
|
value_type = mapping['value_type']
|
|
|
|
if value_type == 'custom_text':
|
|
# Use custom text
|
|
replacements[key] = mapping.get('custom_text', '')
|
|
else:
|
|
# Use data from the data dictionary
|
|
value_field = mapping.get('value_field', '')
|
|
if value_field and value_field in data:
|
|
replacements[key] = str(data[value_field])
|
|
else:
|
|
# Use empty string for unmapped fields
|
|
replacements[key] = ''
|
|
|
|
# Replace placeholders in paragraphs
|
|
for paragraph in doc.paragraphs:
|
|
for key, value in replacements.items():
|
|
if key in paragraph.text:
|
|
# Replace in the paragraph text
|
|
for run in paragraph.runs:
|
|
if key in run.text:
|
|
run.text = run.text.replace(key, value)
|
|
|
|
# Replace placeholders in tables
|
|
for table in doc.tables:
|
|
for row in table.rows:
|
|
for cell in row.cells:
|
|
for paragraph in cell.paragraphs:
|
|
for key, value in replacements.items():
|
|
if key in paragraph.text:
|
|
for run in paragraph.runs:
|
|
if key in run.text:
|
|
run.text = run.text.replace(key, value)
|
|
|
|
# Replace placeholders in headers/footers
|
|
for section in doc.sections:
|
|
# Header
|
|
for paragraph in section.header.paragraphs:
|
|
for key, value in replacements.items():
|
|
if key in paragraph.text:
|
|
for run in paragraph.runs:
|
|
if key in run.text:
|
|
run.text = run.text.replace(key, value)
|
|
|
|
# Footer
|
|
for paragraph in section.footer.paragraphs:
|
|
for key, value in replacements.items():
|
|
if key in paragraph.text:
|
|
for run in paragraph.runs:
|
|
if key in run.text:
|
|
run.text = run.text.replace(key, value)
|
|
|
|
# Save to bytes
|
|
output_stream = io.BytesIO()
|
|
doc.save(output_stream)
|
|
output_stream.seek(0)
|
|
return output_stream.read()
|
|
|
|
|
|
def extract_all_text_from_docx(docx_binary):
|
|
"""Extract all text content from a DOCX document."""
|
|
doc_stream = io.BytesIO(docx_binary)
|
|
doc = Document(doc_stream)
|
|
|
|
text_parts = []
|
|
|
|
# Extract from paragraphs
|
|
for paragraph in doc.paragraphs:
|
|
text_parts.append(paragraph.text)
|
|
|
|
# Extract from tables
|
|
for table in doc.tables:
|
|
for row in table.rows:
|
|
for cell in row.cells:
|
|
for paragraph in cell.paragraphs:
|
|
text_parts.append(paragraph.text)
|
|
|
|
# Extract from headers/footers
|
|
for section in doc.sections:
|
|
for paragraph in section.header.paragraphs:
|
|
text_parts.append(paragraph.text)
|
|
for paragraph in section.footer.paragraphs:
|
|
text_parts.append(paragraph.text)
|
|
|
|
return ' '.join(text_parts)
|
|
|
|
|
|
def test_property_9_preview_generation():
|
|
"""
|
|
Feature: survey-custom-certificate-template, Property 9: Preview generation
|
|
|
|
For any configured template, clicking the preview button should generate
|
|
a sample certificate with all placeholders replaced.
|
|
|
|
Validates: Requirements 4.2
|
|
|
|
This property test verifies that:
|
|
1. Preview generation succeeds for any valid template with mappings
|
|
2. The generated preview is a valid DOCX document
|
|
3. All placeholders in the template are replaced (no placeholders remain)
|
|
4. The preview contains either sample data or custom text for each placeholder
|
|
"""
|
|
print("\nTesting Property 9: Preview generation")
|
|
print("=" * 60)
|
|
|
|
test_count = 0
|
|
failures = []
|
|
|
|
@given(test_case=docx_with_mappings_and_data())
|
|
@settings(
|
|
max_examples=100,
|
|
deadline=None,
|
|
suppress_health_check=[HealthCheck.function_scoped_fixture]
|
|
)
|
|
def check_preview_generation(test_case):
|
|
nonlocal test_count, failures
|
|
test_count += 1
|
|
|
|
docx_binary, mappings, data = test_case
|
|
|
|
# Generate sample data (as the wizard would do)
|
|
sample_data = generate_sample_data()
|
|
|
|
# Merge with test data to ensure we have all fields
|
|
for key, value in data.items():
|
|
if key not in sample_data:
|
|
sample_data[key] = value
|
|
|
|
# Property 1: Preview generation should succeed
|
|
try:
|
|
preview_docx = replace_placeholders_in_docx(docx_binary, mappings, sample_data)
|
|
except Exception as e:
|
|
failures.append(f"Preview generation failed: {e}")
|
|
raise AssertionError(f"Preview generation should succeed for any valid template: {e}")
|
|
|
|
# Property 2: The generated preview should be a valid DOCX
|
|
try:
|
|
preview_stream = io.BytesIO(preview_docx)
|
|
preview_doc = Document(preview_stream)
|
|
# If we can load it, it's valid
|
|
except Exception as e:
|
|
failures.append(f"Generated preview is not a valid DOCX: {e}")
|
|
raise AssertionError(f"Generated preview should be a valid DOCX document: {e}")
|
|
|
|
# Property 3: All placeholders should be replaced (no placeholders remain)
|
|
preview_text = extract_all_text_from_docx(preview_docx)
|
|
|
|
# Check for any remaining placeholders
|
|
import re
|
|
remaining_placeholders = re.findall(r'\{key\.[a-zA-Z0-9_]+\}', preview_text)
|
|
|
|
if remaining_placeholders:
|
|
failures.append(
|
|
f"Preview contains unreplaced placeholders: {remaining_placeholders}"
|
|
)
|
|
raise AssertionError(
|
|
f"All placeholders should be replaced in preview. "
|
|
f"Found unreplaced: {remaining_placeholders}"
|
|
)
|
|
|
|
# Property 4: The preview should contain expected data
|
|
# For each mapping, verify that the replacement value appears in the preview
|
|
for mapping in mappings['placeholders']:
|
|
value_type = mapping['value_type']
|
|
|
|
if value_type == 'custom_text':
|
|
# Should contain the custom text
|
|
expected_value = mapping.get('custom_text', '')
|
|
if expected_value and expected_value not in preview_text:
|
|
# Only fail if the custom text is non-empty
|
|
failures.append(
|
|
f"Preview missing custom text '{expected_value}' for {mapping['key']}"
|
|
)
|
|
raise AssertionError(
|
|
f"Preview should contain custom text '{expected_value}' "
|
|
f"for placeholder {mapping['key']}"
|
|
)
|
|
else:
|
|
# Should contain data from sample_data
|
|
value_field = mapping.get('value_field', '')
|
|
if value_field and value_field in sample_data:
|
|
expected_value = str(sample_data[value_field])
|
|
if expected_value and expected_value not in preview_text:
|
|
# Only fail if the expected value is non-empty
|
|
failures.append(
|
|
f"Preview missing data '{expected_value}' for {mapping['key']}"
|
|
)
|
|
raise AssertionError(
|
|
f"Preview should contain data '{expected_value}' "
|
|
f"for placeholder {mapping['key']}"
|
|
)
|
|
|
|
try:
|
|
check_preview_generation()
|
|
print(f"✓ Property 9 verified across {test_count} test cases")
|
|
print(" All preview generations succeeded with placeholders replaced")
|
|
print(f" Tested with various template structures and mapping configurations")
|
|
return True
|
|
except Exception as e:
|
|
print(f"✗ Property 9 FAILED after {test_count} test cases")
|
|
print(f" Error: {e}")
|
|
if failures:
|
|
print(f"\n Failure details:")
|
|
for failure in failures[:5]: # Show first 5 failures
|
|
print(f" - {failure}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Run the property test."""
|
|
print("=" * 60)
|
|
print("Property-Based Test: Preview Generation")
|
|
print("=" * 60)
|
|
print("\nThis test verifies that preview generation works correctly")
|
|
print("for any configured template with placeholder mappings.")
|
|
print("\nTesting scenarios:")
|
|
print(" - Various template structures (paragraphs, tables, headers/footers)")
|
|
print(" - Different numbers of placeholders (1-8)")
|
|
print(" - Different value types (survey_field, user_field, custom_text)")
|
|
print(" - Placeholder replacement completeness")
|
|
print(" - Valid DOCX output")
|
|
|
|
success = test_property_9_preview_generation()
|
|
|
|
print("\n" + "=" * 60)
|
|
if success:
|
|
print("✓ Property test PASSED")
|
|
print("=" * 60)
|
|
print("\nConclusion:")
|
|
print(" The preview generation mechanism correctly creates sample")
|
|
print(" certificates with all placeholders replaced for any valid")
|
|
print(" template configuration.")
|
|
return 0
|
|
else:
|
|
print("✗ Property test FAILED")
|
|
print("=" * 60)
|
|
print("\nThe preview generation mechanism has issues that need to be addressed.")
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|