"""
Comprehensive integration tests for the ReadBookMom app.
Tests cover:
- Module imports and structure
- Story loading and text processing
- HTML rendering functions (dashboard, library, voices, story text)
- Gradio component wiring (inputs/outputs match handler signatures)
- State machine transitions (book select → play → pause → ask → resume)
- XSS escaping in all HTML renderers
- Voice profile integration in player
- TTS module chunk splitting
- Q&A answer rendering safety
"""
import os
import sys
import html as html_module
import inspect
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
PASS = 0
FAIL = 0
def check(label, condition, detail=""):
global PASS, FAIL
if condition:
PASS += 1
print(f" [OK] {label}")
else:
FAIL += 1
print(f" [FAIL] {label} -- {detail}")
# ── Module imports ──────────────────────────────────────────────
def test_module_imports():
print("\n=== Module imports ===")
try:
from tts import split_into_chunks, generate_audio_stream
check("tts module imports", True)
except Exception as e:
check("tts module imports", False, str(e))
try:
from voice_clone import (
create_voice_profile, synthesize_cloned, synthesize_cloned_preview,
synthesize_custom_voice, has_profile, get_model_mode,
_trim_reference_audio, _select_dtype, _select_attn_impl,
GENERATION_PARAMS, BASE_MODEL_ID, CUSTOM_VOICE_MODEL_ID,
)
check("voice_clone module imports", True)
except Exception as e:
check("voice_clone module imports", False, str(e))
try:
from inference import transcribe_audio, answer_story_question
check("inference module imports", True)
except Exception as e:
check("inference module imports", False, str(e))
# ── Story loading ───────────────────────────────────────────────
def test_story_loading():
print("\n=== Story loading ===")
from app import load_books_from_stories, load_paragraphs
books = load_books_from_stories()
check("Stories found", len(books) > 0, f"found {len(books)}")
for b in books:
required = ["id", "title", "story_path", "cover_url", "voice_name", "percentage"]
missing = [k for k in required if k not in b]
check(f"Book '{b['title']}' has all keys", len(missing) == 0, f"missing: {missing}")
# Test paragraph loading
if books:
paras = load_paragraphs(books[0]["story_path"])
check(f"Paragraphs loaded from '{books[0]['title']}'", len(paras) > 0, f"got {len(paras)}")
# Test non-existent file
paras = load_paragraphs("nonexistent_file.txt")
check("Non-existent file returns empty list", paras == [])
# ── TTS chunk splitting ────────────────────────────────────────
def test_chunk_splitting():
print("\n=== TTS chunk splitting ===")
from tts import split_into_chunks
chunks = split_into_chunks("Hello world. How are you? I am fine!")
check("Splits on sentence boundaries", len(chunks) == 3, f"got {len(chunks)}: {chunks}")
chunks = split_into_chunks("Single sentence without ending period")
check("Single sentence stays intact", len(chunks) == 1, f"got {len(chunks)}")
chunks = split_into_chunks("")
check("Empty text returns empty list", len(chunks) == 0)
chunks = split_into_chunks(" ")
check("Whitespace-only returns empty list", len(chunks) == 0)
long_text = "First sentence. Second sentence! Third? Fourth; Fifth."
chunks = split_into_chunks(long_text)
check("Splits on .!?; delimiters", len(chunks) == 5, f"got {len(chunks)}: {chunks}")
# ── HTML rendering ──────────────────────────────────────────────
def test_html_rendering():
print("\n=== HTML rendering ===")
from app import (
generate_dashboard_html, generate_library_html,
render_cloned_voices_html, render_story_text, mock_books, mock_voices,
)
# Dashboard
dash_html = generate_dashboard_html(mock_books)
check("Dashboard renders without error", isinstance(dash_html, str) and len(dash_html) > 100)
check("Dashboard contains book titles", any(b["title"] in dash_html for b in mock_books))
# Library
lib_html = generate_library_html(mock_books)
check("Library renders without error", isinstance(lib_html, str) and len(lib_html) > 100)
check("Library contains Tap to Play", "Tap to Play" in lib_html)
# Library with search filter
if mock_books:
first_title = mock_books[0]["title"]
filtered = generate_library_html(mock_books, query=first_title)
check(f"Library filters to '{first_title}'", first_title in filtered)
# Library empty result
empty = generate_library_html(mock_books, query="xyznonexistent999")
check("Library shows no-match message", "No audiobooks match" in empty)
# Cloned voices
voices_html = render_cloned_voices_html(mock_voices)
check("Voices grid renders", isinstance(voices_html, str) and len(voices_html) > 50)
# Story text
paras = ["Paragraph one.", "Paragraph two.", "Paragraph three."]
story_html = render_story_text(paras, 1)
check("Story text highlights current paragraph", "border-left: 3px solid #f5841f" in story_html)
check("Story text renders all paragraphs", "Paragraph one" in story_html and "Paragraph three" in story_html)
# Empty paragraphs
check("Empty paragraphs returns empty string", render_story_text([], 0) == "")
# ── XSS escaping ───────────────────────────────────────────────
def test_xss_escaping():
print("\n=== XSS escaping ===")
from app import render_story_text, render_cloned_voices_html
# Story text with HTML injection
xss_paras = ['', 'Normal text']
result = render_story_text(xss_paras, 0)
check("Story text escapes