This commit is contained in:
386
tests/test_html_rendering.py
Normal file
386
tests/test_html_rendering.py
Normal file
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test HTML Rendering in Notes Field
|
||||
|
||||
This script tests that the notes field HTML content is properly rendered
|
||||
in the output HTML file instead of being escaped.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Add the current directory to the path so we can import modules
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from snapshot_downloader import SnapshotDownloader
|
||||
|
||||
|
||||
class HTMLRenderingTester:
|
||||
"""Test class for HTML rendering functionality."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the tester."""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def test_notes_html_rendering(self):
|
||||
"""Test that HTML in notes field is properly rendered."""
|
||||
print("=" * 60)
|
||||
print("TEST: HTML Rendering in Notes Field")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||||
|
||||
print("1. Testing snapshot with HTML content in notes...")
|
||||
|
||||
# Create mock snapshot with HTML content in notes
|
||||
mock_snapshot = {
|
||||
"id": "test_html_rendering",
|
||||
"type": "Snapshot",
|
||||
"code": "Snapshot",
|
||||
"child": {
|
||||
"forename": "Test",
|
||||
"surname": "Child"
|
||||
},
|
||||
"author": {
|
||||
"forename": "Test",
|
||||
"surname": "Teacher"
|
||||
},
|
||||
"startTime": "2024-01-15T10:30:00",
|
||||
"notes": """<p>This is a <strong>bold</strong> statement about the child's progress.</p>
|
||||
<p><br></p>
|
||||
<p>The child demonstrated <em>excellent</em> skills in:</p>
|
||||
<p>• Communication</p>
|
||||
<p>• Problem solving</p>
|
||||
<p><br></p>
|
||||
<p><span style="color: rgb(255, 0, 0);">Important note:</span> Continue encouraging creative play.</p>
|
||||
<p><span style="font-size: 14px;">Next steps: Focus on fine motor skills development.</span></p>""",
|
||||
"frameworkIndicatorCount": 15,
|
||||
"signed": False
|
||||
}
|
||||
|
||||
# Generate HTML for the snapshot
|
||||
html_content = downloader.format_snapshot_html(mock_snapshot)
|
||||
|
||||
print("2. Checking HTML content rendering...")
|
||||
|
||||
# Check that HTML tags are NOT escaped (should be rendered) within notes-content
|
||||
if 'notes-content"><p>' in html_content or 'notes-content"><strong>' in html_content:
|
||||
print(" ✅ HTML paragraph tags are rendered (not escaped)")
|
||||
else:
|
||||
print(" ❌ HTML paragraph tags are escaped instead of rendered")
|
||||
# Debug output to see what we actually got
|
||||
start = html_content.find('notes-content')
|
||||
if start != -1:
|
||||
sample = html_content[start:start+150]
|
||||
print(f" Debug - Found: {sample}")
|
||||
return False
|
||||
|
||||
if "<strong>bold</strong>" in html_content:
|
||||
print(" ✅ HTML strong tags are rendered (not escaped)")
|
||||
else:
|
||||
print(" ❌ HTML strong tags are escaped instead of rendered")
|
||||
return False
|
||||
|
||||
if "<em>excellent</em>" in html_content:
|
||||
print(" ✅ HTML emphasis tags are rendered (not escaped)")
|
||||
else:
|
||||
print(" ❌ HTML emphasis tags are escaped instead of rendered")
|
||||
return False
|
||||
|
||||
if 'style="color: rgb(255, 0, 0);"' in html_content:
|
||||
print(" ✅ Inline CSS styles are preserved")
|
||||
else:
|
||||
print(" ❌ Inline CSS styles are not preserved")
|
||||
return False
|
||||
|
||||
print("\n3. Testing complete HTML file generation...")
|
||||
|
||||
# Generate complete HTML file
|
||||
mock_snapshots = [mock_snapshot]
|
||||
html_file = downloader.generate_html_file(
|
||||
mock_snapshots, "2024-01-01", "2024-01-31"
|
||||
)
|
||||
|
||||
if html_file.exists():
|
||||
print(" ✅ HTML file created successfully")
|
||||
|
||||
# Read and check file content
|
||||
with open(html_file, 'r', encoding='utf-8') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# Check for proper HTML structure
|
||||
if 'class="notes-content"' in file_content:
|
||||
print(" ✅ Notes content wrapper class present")
|
||||
else:
|
||||
print(" ❌ Notes content wrapper class missing")
|
||||
return False
|
||||
|
||||
# Check that HTML content is rendered in the file
|
||||
if "<p>This is a <strong>bold</strong> statement" in file_content:
|
||||
print(" ✅ HTML content properly rendered in file")
|
||||
else:
|
||||
print(" ❌ HTML content not properly rendered in file")
|
||||
print(" Debug: Looking for HTML content in file...")
|
||||
# Show a sample of the content for debugging
|
||||
start = file_content.find('notes-content')
|
||||
if start != -1:
|
||||
sample = file_content[start:start+200]
|
||||
print(f" Sample content: {sample}")
|
||||
return False
|
||||
|
||||
# Check for CSS styles that handle HTML content
|
||||
if ".notes-content" in file_content:
|
||||
print(" ✅ CSS styles for notes content included")
|
||||
else:
|
||||
print(" ❌ CSS styles for notes content missing")
|
||||
return False
|
||||
|
||||
else:
|
||||
print(" ❌ HTML file was not created")
|
||||
return False
|
||||
|
||||
print("\n4. Testing XSS safety with potentially dangerous content...")
|
||||
|
||||
# Test with potentially dangerous content to ensure basic safety
|
||||
dangerous_snapshot = {
|
||||
"id": "test_xss_safety",
|
||||
"type": "Snapshot",
|
||||
"startTime": "2024-01-15T10:30:00",
|
||||
"notes": '<p>Safe content</p><script>alert("xss")</script><p>More safe content</p>',
|
||||
}
|
||||
|
||||
dangerous_html = downloader.format_snapshot_html(dangerous_snapshot)
|
||||
|
||||
# The script tag should still be present (we're not sanitizing, just rendering)
|
||||
# But we should document this as a security consideration
|
||||
if '<script>' in dangerous_html:
|
||||
print(" ⚠️ Script tags are rendered (consider content sanitization for production)")
|
||||
else:
|
||||
print(" ✅ Script tags are filtered/escaped")
|
||||
|
||||
print("\n✅ HTML rendering test completed!")
|
||||
return True
|
||||
|
||||
def test_complex_html_scenarios(self):
|
||||
"""Test various complex HTML scenarios."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Complex HTML Scenarios")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "Nested HTML Tags",
|
||||
"notes": '<p>Child showed <strong>excellent <em>progress</em></strong> today.</p>',
|
||||
"should_contain": ['<strong>', '<em>', 'excellent', 'progress']
|
||||
},
|
||||
{
|
||||
"name": "Line Breaks and Paragraphs",
|
||||
"notes": '<p>First paragraph.</p><p><br></p><p>Second paragraph after break.</p>',
|
||||
"should_contain": ['<p>First paragraph.</p>', '<p><br></p>', '<p>Second paragraph']
|
||||
},
|
||||
{
|
||||
"name": "Styled Text",
|
||||
"notes": '<p><span style="color: rgb(0, 0, 255); font-size: 16px;">Blue text</span></p>',
|
||||
"should_contain": ['style="color: rgb(0, 0, 255)', 'font-size: 16px', 'Blue text']
|
||||
},
|
||||
{
|
||||
"name": "Mixed Content",
|
||||
"notes": '<p>Normal text</p><p>• Bullet point 1</p><p>• Bullet point 2</p><p><strong>Next steps:</strong> Continue activities.</p>',
|
||||
"should_contain": ['Normal text', '• Bullet', '<strong>Next steps:</strong>']
|
||||
}
|
||||
]
|
||||
|
||||
for i, test_case in enumerate(test_cases, 1):
|
||||
print(f"\n{i}. Testing: {test_case['name']}")
|
||||
|
||||
mock_snapshot = {
|
||||
"id": f"test_case_{i}",
|
||||
"type": "Snapshot",
|
||||
"startTime": "2024-01-15T10:30:00",
|
||||
"notes": test_case['notes']
|
||||
}
|
||||
|
||||
html_content = downloader.format_snapshot_html(mock_snapshot)
|
||||
|
||||
# Check that all expected content is present and rendered
|
||||
all_found = True
|
||||
for expected in test_case['should_contain']:
|
||||
if expected in html_content:
|
||||
print(f" ✅ Found: {expected[:30]}...")
|
||||
else:
|
||||
print(f" ❌ Missing: {expected[:30]}...")
|
||||
all_found = False
|
||||
|
||||
if not all_found:
|
||||
print(f" ❌ Test case '{test_case['name']}' failed")
|
||||
return False
|
||||
else:
|
||||
print(f" ✅ Test case '{test_case['name']}' passed")
|
||||
|
||||
print("\n✅ Complex HTML scenarios test completed!")
|
||||
return True
|
||||
|
||||
def test_empty_and_edge_cases(self):
|
||||
"""Test edge cases for notes field."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST: Edge Cases")
|
||||
print("=" * 60)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
downloader = SnapshotDownloader(output_dir=temp_dir)
|
||||
|
||||
edge_cases = [
|
||||
{
|
||||
"name": "Empty notes",
|
||||
"notes": "",
|
||||
"expected": "No description provided"
|
||||
},
|
||||
{
|
||||
"name": "None notes",
|
||||
"notes": None,
|
||||
"expected": "No description provided"
|
||||
},
|
||||
{
|
||||
"name": "Only whitespace",
|
||||
"notes": " \n\t ",
|
||||
"expected": " \n\t " # Should preserve whitespace
|
||||
},
|
||||
{
|
||||
"name": "Plain text (no HTML)",
|
||||
"notes": "Just plain text without HTML tags.",
|
||||
"expected": "Just plain text without HTML tags."
|
||||
}
|
||||
]
|
||||
|
||||
for i, test_case in enumerate(edge_cases, 1):
|
||||
print(f"\n{i}. Testing: {test_case['name']}")
|
||||
|
||||
mock_snapshot = {
|
||||
"id": f"edge_case_{i}",
|
||||
"type": "Snapshot",
|
||||
"startTime": "2024-01-15T10:30:00"
|
||||
}
|
||||
|
||||
if test_case['notes'] is not None:
|
||||
mock_snapshot['notes'] = test_case['notes']
|
||||
|
||||
html_content = downloader.format_snapshot_html(mock_snapshot)
|
||||
|
||||
if test_case['expected'] in html_content:
|
||||
print(f" ✅ Correctly handled: {test_case['name']}")
|
||||
else:
|
||||
print(f" ❌ Failed: {test_case['name']}")
|
||||
print(f" Expected: {test_case['expected']}")
|
||||
# Show relevant part of HTML for debugging
|
||||
start = html_content.find('notes-content')
|
||||
if start != -1:
|
||||
sample = html_content[start:start+100]
|
||||
print(f" Found: {sample}")
|
||||
return False
|
||||
|
||||
print("\n✅ Edge cases test completed!")
|
||||
return True
|
||||
|
||||
def run_all_tests(self):
|
||||
"""Run all HTML rendering tests."""
|
||||
print("🚀 Starting HTML Rendering Tests")
|
||||
print("=" * 80)
|
||||
|
||||
try:
|
||||
success = True
|
||||
|
||||
success &= self.test_notes_html_rendering()
|
||||
success &= self.test_complex_html_scenarios()
|
||||
success &= self.test_empty_and_edge_cases()
|
||||
|
||||
if success:
|
||||
print("\n" + "=" * 80)
|
||||
print("🎉 ALL HTML RENDERING TESTS PASSED!")
|
||||
print("=" * 80)
|
||||
print("✅ HTML content in notes field is properly rendered")
|
||||
print("✅ Complex HTML scenarios work correctly")
|
||||
print("✅ Edge cases are handled appropriately")
|
||||
print("✅ CSS styles support HTML content rendering")
|
||||
print("\n⚠️ Security Note:")
|
||||
print(" HTML content is rendered as-is for rich formatting.")
|
||||
print(" Consider content sanitization if accepting user input.")
|
||||
else:
|
||||
print("\n❌ SOME HTML RENDERING TESTS FAILED")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ HTML RENDERING TESTS FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def show_html_rendering_info():
|
||||
"""Show information about HTML rendering in notes."""
|
||||
print("\n" + "=" * 80)
|
||||
print("📝 HTML RENDERING IN NOTES FIELD")
|
||||
print("=" * 80)
|
||||
|
||||
print("\n🎨 What's Rendered:")
|
||||
print("• <p> tags for paragraphs")
|
||||
print("• <strong> and <em> for bold/italic text")
|
||||
print("• <br> tags for line breaks")
|
||||
print("• <span> with style attributes for colors/fonts")
|
||||
print("• Bullet points and lists")
|
||||
print("• All inline CSS styles")
|
||||
|
||||
print("\n💡 Example HTML Content:")
|
||||
print('<p>Child showed <strong>excellent</strong> progress today.</p>')
|
||||
print('<p><br></p>')
|
||||
print('<p><span style="color: rgb(255, 0, 0);">Important:</span> Continue activities.</p>')
|
||||
|
||||
print("\n📋 Becomes:")
|
||||
print("Child showed excellent progress today.")
|
||||
print("")
|
||||
print("Important: Continue activities. (in red)")
|
||||
|
||||
print("\n🔒 Security Considerations:")
|
||||
print("• HTML content is rendered as-is from the API")
|
||||
print("• Content comes from trusted ParentZone staff")
|
||||
print("• Script tags and other content are preserved")
|
||||
print("• Consider sanitization for untrusted input")
|
||||
|
||||
print("\n🎯 Benefits:")
|
||||
print("• Rich text formatting preserved")
|
||||
print("• Professional-looking reports")
|
||||
print("• Colors and styling from original content")
|
||||
print("• Better readability and presentation")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main test function."""
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
tester = HTMLRenderingTester()
|
||||
|
||||
# Run tests
|
||||
success = tester.run_all_tests()
|
||||
|
||||
# Show information
|
||||
if success:
|
||||
show_html_rendering_info()
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user