NarratoAI/tests/test_streamlit_widget_session_state.py
viccy 990fb36e98 fix(webui, tests): 修复硬编码路径与组件默认值问题并新增测试
替换音频设置模块的硬编码本地路径为资源目录工具函数,提升跨平台兼容性
修正字幕预览方向选择组件的默认值处理逻辑,移除冗余的默认参数
新增Streamlit组件会话状态合规性测试,确保带key的组件不会同时声明default参数
更新.gitignore以允许提交新增的测试文件
2026-07-13 03:48:05 +08:00

85 lines
2.5 KiB
Python

"""Regression coverage for Streamlit widget state initialization."""
import ast
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).parents[1]
def _attribute_name(node: ast.expr) -> str | None:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
parent = _attribute_name(node.value)
return f"{parent}.{node.attr}" if parent else node.attr
return None
def _session_state_key(target: ast.expr) -> str | None:
if not isinstance(target, ast.Subscript):
return None
if _attribute_name(target.value) != "st.session_state":
return None
if isinstance(target.slice, ast.Constant) and isinstance(target.slice.value, str):
return target.slice.value
return None
@pytest.mark.parametrize(
("source_path", "function_name", "widget_key"),
[
(
"webui/components/audio_settings.py",
"render_bgm_settings",
"bgm_source_selection",
),
(
"webui/components/subtitle_settings.py",
"_render_subtitle_preview_image",
"subtitle_preview_orientation",
),
],
)
def test_state_initialized_widgets_do_not_also_declare_defaults(
source_path: str,
function_name: str,
widget_key: str,
):
"""Streamlit warns when a keyed widget has both a default and state value."""
module = ast.parse((PROJECT_ROOT / source_path).read_text(encoding="utf-8"))
function = next(
node
for node in module.body
if isinstance(node, ast.FunctionDef) and node.name == function_name
)
assigned_keys = {
key
for node in ast.walk(function)
if isinstance(node, ast.Assign)
for target in node.targets
if (key := _session_state_key(target)) is not None
}
assert widget_key in assigned_keys
widget_calls = []
for node in ast.walk(function):
if not isinstance(node, ast.Call):
continue
widget_name = _attribute_name(node.func)
keywords = {keyword.arg: keyword.value for keyword in node.keywords}
key_node = keywords.get("key")
if (
widget_name is not None
and widget_name.startswith("st.")
and isinstance(key_node, ast.Constant)
and key_node.value == widget_key
):
widget_calls.append(keywords)
assert widget_calls
assert all("default" not in keywords for keywords in widget_calls)