Spaces:
Paused
Paused
File size: 13,053 Bytes
122c643 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | """
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 = ['<script>alert("xss")</script>', 'Normal text']
result = render_story_text(xss_paras, 0)
check("Story text escapes <script>", "<script>" not in result)
check("Story text contains escaped version", "<script>" in result)
# Voices with HTML injection
xss_voices = [{
"id": "v1", "name": '<img onerror="alert(1)" src=x>',
"avatar_url": "π©", "description": "test", "gender": "Female",
"created_date": "2026-01-01", "status": "ready"
}]
result = render_cloned_voices_html(xss_voices)
check("Voice name escapes HTML", "<img onerror" not in result)
check("Voice name contains escaped version", "<img" in result)
# ββ Gradio component wiring ββββββββββββββββββββββββββββββββββββ
def test_gradio_wiring():
print("\n=== Gradio component wiring ===")
from app import demo
# Verify the demo has the expected blocks
blocks = demo.blocks
check("Demo has blocks", len(blocks) > 0, f"found {len(blocks)}")
# Check that no component references a deleted 'loading_spinner'
source = open("app.py", encoding="utf-8").read()
check("No loading_spinner references", "loading_spinner" not in source)
check("No empty_state_sim references", "empty_state_sim" not in source)
check("No offline_toggle references", "offline_toggle" not in source)
check("No system_error_modal references", "system_error_modal" not in source)
check("No narrative_subtitles references", "narrative_subtitles" not in source)
# ββ Handler signatures vs outputs ββββββββββββββββββββββββββββββ
def test_handler_signatures():
print("\n=== Handler signatures ===")
source = open("app.py", encoding="utf-8").read()
# Verify key handler functions exist
handlers = [
"def filter_books_shelf",
"def handle_book_select",
"def animate_cloning_pipeline",
"def save_new_cloned_voice",
"def stream_tts",
"def enter_paused_state",
"def update_story_highlight",
"def enter_asking_state",
"def handle_question_submit",
"def enter_resume_state",
]
for h in handlers:
check(f"Handler {h.split('def ')[1]} exists", h in source)
# Verify removed handlers are gone
check("check_story_finished removed (inline in stream_tts)", "def check_story_finished" not in source)
check("toggle_outage_sandbox removed", "def toggle_outage_sandbox" not in source)
# ββ Voice profile integration ββββββββββββββββββββββββββββββββββ
def test_voice_profile_integration():
print("\n=== Voice profile integration ===")
from voice_clone import has_profile
check("has_profile(None) is False", has_profile(None) is False)
check("has_profile('') is False", has_profile("") is False)
check("has_profile('nonexistent') is False", has_profile("nonexistent") is False)
# Verify handle_book_select accepts profile_id
source = open("app.py", encoding="utf-8").read()
check("handle_book_select takes profile_id",
"def handle_book_select(title_chosen, current_inventory, profile_id)" in source)
check("book_selector inputs include voice_profile_state",
"inputs=[book_selector, books_state, voice_profile_state]" in source)
# ββ State machine consistency ββββββββββββββββββββββββββββββββββ
def test_state_machine():
print("\n=== State machine consistency ===")
source = open("app.py", encoding="utf-8").read()
# Play β shows pause_btn, hides play_btn
check("stream_tts hides play_btn during playback",
"gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True)" in source)
# Pause β shows play_btn, hides pause_btn
check("enter_paused_state shows play_btn",
"gr.Button(visible=True),\n gr.Button(visible=False)," in source or
("enter_paused_state" in source and "gr.Button(visible=True)" in source))
# Resume β shows play_btn (ready state), hides resume_btn
check("enter_resume_state shows ready state",
"READY" in source and "Tap Play to continue" in source)
# Ask β shows resume_btn, hides play/pause/ask
check("enter_asking_state shows resume_btn",
"PAUSED" in source and "ASKING" in source)
# stream_tts updates timeline_slider
check("stream_tts outputs include timeline_slider",
"timeline_slider, story_text_display, story_finished_panel" in source)
# ββ App launch readiness βββββββββββββββββββββββββββββββββββββββ
def test_app_launch():
print("\n=== App launch readiness ===")
from app import demo
check("Demo object exists", demo is not None)
check("Demo is a gr.Blocks instance", "Blocks" in type(demo).__name__)
# Check allowed_paths in launch call
source = open("app.py", encoding="utf-8").read()
check("Launch has allowed_paths for assets", 'allowed_paths' in source)
check("Launch uses .queue()", '.queue()' in source)
# ββ File structure ββββββββββββββββββββββββββββββββββββββββββββββ
def test_file_structure():
print("\n=== File structure ===")
required_files = [
"app.py", "tts.py", "voice_clone.py", "inference.py",
"requirements.txt", "static/style.css",
]
for f in required_files:
path = os.path.join(os.path.dirname(os.path.dirname(__file__)), f)
check(f"File exists: {f}", os.path.exists(path))
# Check stories directory has content
stories_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "stories")
if os.path.isdir(stories_dir):
txt_files = [f for f in os.listdir(stories_dir) if f.endswith(".txt")]
check("Stories directory has .txt files", len(txt_files) > 0, f"found {len(txt_files)}")
else:
check("Stories directory exists", False, "stories/ not found")
if __name__ == "__main__":
print("=" * 60)
print("Comprehensive Integration Tests β ReadBookMom")
print("=" * 60)
test_module_imports()
test_story_loading()
test_chunk_splitting()
test_html_rendering()
test_xss_escaping()
test_gradio_wiring()
test_handler_signatures()
test_voice_profile_integration()
test_state_machine()
test_app_launch()
test_file_structure()
print("\n" + "=" * 60)
print(f"Results: {PASS} passed, {FAIL} failed")
print("=" * 60)
sys.exit(1 if FAIL > 0 else 0)
|