Files
celery/tests/test_effects_web.py
gilesb 2cc7d88d2e Add effects count to home page dashboard
- Add Effects card to home page grid (now 3 columns)
- Add stats["effects"] count from cache.list_by_type('effect')
- Add tests for home page effects display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 12:19:06 +00:00

368 lines
13 KiB
Python

"""
Tests for Effects web UI.
Tests effect metadata parsing, listing, and templates.
"""
import pytest
import re
from pathlib import Path
from unittest.mock import MagicMock
def parse_effect_metadata_standalone(source: str) -> dict:
"""
Standalone copy of parse_effect_metadata for testing.
This avoids import issues with the router module.
"""
metadata = {
"name": "",
"version": "1.0.0",
"author": "",
"temporal": False,
"description": "",
"params": [],
"dependencies": [],
"requires_python": ">=3.10",
}
# Parse PEP 723 dependencies
pep723_match = re.search(r"# /// script\n(.*?)# ///", source, re.DOTALL)
if pep723_match:
block = pep723_match.group(1)
deps_match = re.search(r'# dependencies = \[(.*?)\]', block, re.DOTALL)
if deps_match:
metadata["dependencies"] = re.findall(r'"([^"]+)"', deps_match.group(1))
python_match = re.search(r'# requires-python = "([^"]+)"', block)
if python_match:
metadata["requires_python"] = python_match.group(1)
# Parse docstring @-tags
docstring_match = re.search(r'"""(.*?)"""', source, re.DOTALL)
if not docstring_match:
docstring_match = re.search(r"'''(.*?)'''", source, re.DOTALL)
if docstring_match:
docstring = docstring_match.group(1)
lines = docstring.split("\n")
current_param = None
desc_lines = []
in_description = False
for line in lines:
stripped = line.strip()
if stripped.startswith("@effect "):
metadata["name"] = stripped[8:].strip()
in_description = False
elif stripped.startswith("@version "):
metadata["version"] = stripped[9:].strip()
elif stripped.startswith("@author "):
metadata["author"] = stripped[8:].strip()
elif stripped.startswith("@temporal "):
val = stripped[10:].strip().lower()
metadata["temporal"] = val in ("true", "yes", "1")
elif stripped.startswith("@description"):
in_description = True
desc_lines = []
elif stripped.startswith("@param "):
if in_description:
metadata["description"] = " ".join(desc_lines)
in_description = False
if current_param:
metadata["params"].append(current_param)
parts = stripped[7:].split()
if len(parts) >= 2:
current_param = {
"name": parts[0],
"type": parts[1],
"description": "",
}
else:
current_param = None
elif stripped.startswith("@range ") and current_param:
range_parts = stripped[7:].split()
if len(range_parts) >= 2:
try:
current_param["range"] = [float(range_parts[0]), float(range_parts[1])]
except ValueError:
pass
elif stripped.startswith("@default ") and current_param:
current_param["default"] = stripped[9:].strip()
elif stripped.startswith("@example"):
if in_description:
metadata["description"] = " ".join(desc_lines)
in_description = False
if current_param:
metadata["params"].append(current_param)
current_param = None
elif in_description and stripped:
desc_lines.append(stripped)
elif current_param and stripped and not stripped.startswith("@"):
current_param["description"] = stripped
if in_description:
metadata["description"] = " ".join(desc_lines)
if current_param:
metadata["params"].append(current_param)
return metadata
class TestEffectMetadataParsing:
"""Tests for parse_effect_metadata function."""
def test_parses_pep723_dependencies(self) -> None:
"""Should extract dependencies from PEP 723 script block."""
source = '''
# /// script
# requires-python = ">=3.10"
# dependencies = ["numpy", "opencv-python"]
# ///
"""
@effect test_effect
"""
def process_frame(frame, params, state):
return frame, state
'''
meta = parse_effect_metadata_standalone(source)
assert meta["dependencies"] == ["numpy", "opencv-python"]
assert meta["requires_python"] == ">=3.10"
def test_parses_effect_name(self) -> None:
"""Should extract effect name from @effect tag."""
source = '''
"""
@effect brightness
@version 2.0.0
@author @artist@example.com
"""
def process_frame(frame, params, state):
return frame, state
'''
meta = parse_effect_metadata_standalone(source)
assert meta["name"] == "brightness"
assert meta["version"] == "2.0.0"
assert meta["author"] == "@artist@example.com"
def test_parses_parameters(self) -> None:
"""Should extract parameter definitions."""
source = '''
"""
@effect brightness
@param level float
@range -1.0 1.0
@default 0.0
Brightness adjustment level
"""
def process_frame(frame, params, state):
return frame, state
'''
meta = parse_effect_metadata_standalone(source)
assert len(meta["params"]) == 1
param = meta["params"][0]
assert param["name"] == "level"
assert param["type"] == "float"
assert param["range"] == [-1.0, 1.0]
assert param["default"] == "0.0"
def test_parses_temporal_flag(self) -> None:
"""Should parse temporal flag correctly."""
source_temporal = '''
"""
@effect motion_blur
@temporal true
"""
def process_frame(frame, params, state):
return frame, state
'''
source_not_temporal = '''
"""
@effect brightness
@temporal false
"""
def process_frame(frame, params, state):
return frame, state
'''
assert parse_effect_metadata_standalone(source_temporal)["temporal"] is True
assert parse_effect_metadata_standalone(source_not_temporal)["temporal"] is False
def test_handles_missing_metadata(self) -> None:
"""Should return sensible defaults for minimal source."""
source = '''
def process_frame(frame, params, state):
return frame, state
'''
meta = parse_effect_metadata_standalone(source)
assert meta["name"] == ""
assert meta["version"] == "1.0.0"
assert meta["dependencies"] == []
assert meta["params"] == []
def test_parses_description(self) -> None:
"""Should extract description text."""
source = '''
"""
@effect test
@description
This is a multi-line
description of the effect.
@param x float
"""
def process_frame(frame, params, state):
return frame, state
'''
meta = parse_effect_metadata_standalone(source)
assert "multi-line" in meta["description"]
class TestHomePageEffectsCount:
"""Test that effects count is shown on home page."""
def test_home_template_has_effects_card(self) -> None:
"""Home page should display effects count."""
path = Path('/home/giles/art/art-celery/app/templates/home.html')
content = path.read_text()
assert 'stats.effects' in content, \
"Home page should display stats.effects count"
assert 'href="/effects"' in content, \
"Home page should link to /effects"
def test_home_route_provides_effects_count(self) -> None:
"""Home route should provide effects count in stats."""
path = Path('/home/giles/art/art-celery/app/routers/home.py')
content = path.read_text()
assert 'stats["effects"]' in content, \
"Home route should populate stats['effects']"
class TestNavigationIncludesEffects:
"""Test that Effects link is in navigation."""
def test_base_template_has_effects_link(self) -> None:
"""Base template should have Effects navigation link."""
base_path = Path('/home/giles/art/art-celery/app/templates/base.html')
content = base_path.read_text()
assert 'href="/effects"' in content
assert "Effects" in content
assert "active_tab == 'effects'" in content
def test_effects_link_between_recipes_and_media(self) -> None:
"""Effects link should be positioned between Recipes and Media."""
base_path = Path('/home/giles/art/art-celery/app/templates/base.html')
content = base_path.read_text()
recipes_pos = content.find('href="/recipes"')
effects_pos = content.find('href="/effects"')
media_pos = content.find('href="/media"')
assert recipes_pos < effects_pos < media_pos, \
"Effects link should be between Recipes and Media"
class TestEffectsTemplatesExist:
"""Tests for effects template files."""
def test_list_template_exists(self) -> None:
"""List template should exist."""
path = Path('/home/giles/art/art-celery/app/templates/effects/list.html')
assert path.exists(), "effects/list.html template should exist"
def test_detail_template_exists(self) -> None:
"""Detail template should exist."""
path = Path('/home/giles/art/art-celery/app/templates/effects/detail.html')
assert path.exists(), "effects/detail.html template should exist"
def test_list_template_extends_base(self) -> None:
"""List template should extend base.html."""
path = Path('/home/giles/art/art-celery/app/templates/effects/list.html')
content = path.read_text()
assert '{% extends "base.html" %}' in content
def test_detail_template_extends_base(self) -> None:
"""Detail template should extend base.html."""
path = Path('/home/giles/art/art-celery/app/templates/effects/detail.html')
content = path.read_text()
assert '{% extends "base.html" %}' in content
def test_list_template_has_upload_button(self) -> None:
"""List template should have upload functionality."""
path = Path('/home/giles/art/art-celery/app/templates/effects/list.html')
content = path.read_text()
assert 'Upload Effect' in content or 'upload' in content.lower()
def test_detail_template_shows_parameters(self) -> None:
"""Detail template should display parameters."""
path = Path('/home/giles/art/art-celery/app/templates/effects/detail.html')
content = path.read_text()
assert 'params' in content.lower() or 'parameter' in content.lower()
def test_detail_template_shows_source_code(self) -> None:
"""Detail template should show source code."""
path = Path('/home/giles/art/art-celery/app/templates/effects/detail.html')
content = path.read_text()
assert 'source' in content.lower()
assert 'language-python' in content
def test_detail_template_shows_dependencies(self) -> None:
"""Detail template should display dependencies."""
path = Path('/home/giles/art/art-celery/app/templates/effects/detail.html')
content = path.read_text()
assert 'dependencies' in content.lower()
class TestEffectsRouterExists:
"""Tests for effects router configuration."""
def test_effects_router_file_exists(self) -> None:
"""Effects router should exist."""
path = Path('/home/giles/art/art-celery/app/routers/effects.py')
assert path.exists(), "effects.py router should exist"
def test_effects_router_has_list_endpoint(self) -> None:
"""Effects router should have list endpoint."""
path = Path('/home/giles/art/art-celery/app/routers/effects.py')
content = path.read_text()
assert '@router.get("")' in content or "@router.get('')" in content
def test_effects_router_has_detail_endpoint(self) -> None:
"""Effects router should have detail endpoint."""
path = Path('/home/giles/art/art-celery/app/routers/effects.py')
content = path.read_text()
assert '@router.get("/{cid}")' in content
def test_effects_router_has_upload_endpoint(self) -> None:
"""Effects router should have upload endpoint."""
path = Path('/home/giles/art/art-celery/app/routers/effects.py')
content = path.read_text()
assert '@router.post("/upload")' in content
def test_effects_router_renders_templates(self) -> None:
"""Effects router should render HTML templates."""
path = Path('/home/giles/art/art-celery/app/routers/effects.py')
content = path.read_text()
assert 'effects/list.html' in content
assert 'effects/detail.html' in content