Files
transcription/tests/test_ui_theme.py
T
zoltan57andCopilot App 6a3ee26733 V4.6 Phase 5: UI boundaries and duplication
Fixes the three ui.instructions.md violations recorded as [HIGH-07] and extracts
the page-level duplication catalogued in review section 4.

Boundary violations
- jobs_page no longer imports session_scope or manages a transaction.
  store.create_document_job and store.create_job_for_document accept an optional
  session_factory and open their own session scope when the caller supplies
  neither a session nor a factory.
- sources_page no longer calls sqlalchemy.inspect. SourceService
  .read_latest_execution_attempt now returns a LatestExecutionAttempt read model
  carrying a plain transport_body_deferred flag, so ORM loader state stays inside
  the service. Rendered output is unchanged.
- Deletes ui/components/document_panzoom.py, its export, and its CSS. The
  component was exported but used by no page. Pan-zoom is planned for a clean
  reintroduction in V4.7 alongside the other photo/image work.

Extracted duplication
- ui/components/media_urls.py: pure upload-URL resolution taking upload_dir and
  base_url, replacing two identical ~60-line copies in sources_page and
  people_page.
- ui/components/guards.py: parse-then-render-terminal-message, replacing 28
  hand-written guard labels across five pages.
- ui/components/confirm_delete.py: the blocked-dependency notice and the
  delete/cancel action row, from four delete pages.
- ui/components/upload_panel.py: the auto-uploading file picker, from three
  pages. Source accept lists now derive from services.source_media
  .SOURCE_EXTENSIONS instead of being hard-coded.
- ui/components/table/registry.py: the two hand-rolled label-registry tables on
  the settings page now go through build_table, which gained selection and
  rows_per_page options.
- ui/components/formatters.py gains parse_uuid and parse_iso_date, replacing
  five and two private copies.
- ui/runtime.py owns resolve_runtime_settings, replacing three copies and
  removing get_settings from every page module.

[LOW-05]
- Upload handlers are annotated with events.UploadEventArguments.
- The Document and Person form builders return DocumentFormFields and
  PersonFormFields dataclasses instead of dict[str, Any].

Verification
- tests/test_ui_boundaries.py asserts no page imports a session scope, a session
  factory, get_settings, sqlalchemy, or sqlmodel, and that no component imports
  request or application state.
- 275 passed, 4 skipped. ruff check clean.

Findings: HIGH-07, LOW-05

Co-authored-by: Copilot App <[email protected]>
2026-08-17 17:44:39 -05:00

82 lines
2.3 KiB
Python

"""Tests for global UI theme registration."""
import re
from pathlib import Path
import pytest
from fastapi import FastAPI
from transcription.ui import register_pages
from transcription.ui.resources import read_css
UI_ROOT = Path(__file__).parents[1] / "src" / "transcription" / "ui"
@pytest.mark.unit
def test_page_registration_uses_vibescribe_theme(monkeypatch):
"""Global UI registration loads the standalone VibeScribe theme in light mode."""
registered_css: list[str] = []
run_options: dict[str, object] = {}
monkeypatch.setattr("transcription.ui.ui.add_css", lambda css, **_kwargs: registered_css.append(css))
monkeypatch.setattr("transcription.ui.register_jobs_page", lambda: None)
monkeypatch.setattr(
"transcription.ui.ui.run_with",
lambda _app, **options: run_options.update(options),
)
register_pages(FastAPI())
theme_css = read_css("theme.css")
assert registered_css == [theme_css]
assert set(re.findall(r"#[0-9a-fA-F]{6}", theme_css)) == {
"#1c2321",
"#7d98a1",
"#5e6572",
"#a9b4c2",
"#eef1ef",
}
assert "--q-primary" in theme_css
assert run_options["dark"] is False
@pytest.mark.unit
def test_theme_is_the_only_ui_stylesheet():
stylesheets = sorted(path.relative_to(UI_ROOT).as_posix() for path in UI_ROOT.rglob("*.css"))
assert stylesheets == ["static/theme.css"]
@pytest.mark.unit
def test_ui_python_uses_class_driven_theme():
prohibited_patterns = {
".style(": "inline NiceGUI style",
"<style": "embedded style block",
"vibe-": "legacy presentation class",
}
violations: list[str] = []
for path in sorted((*UI_ROOT.glob("components/**/*.py"), *UI_ROOT.glob("pages/**/*.py"))):
source = path.read_text(encoding="utf-8")
for pattern, description in prohibited_patterns.items():
if pattern in source:
violations.append(f"{path.relative_to(UI_ROOT)}: {description}")
assert violations == []
@pytest.mark.unit
def test_theme_defines_shared_semantic_surfaces():
theme_css = read_css("theme.css")
for class_name in (
"ui-card-surface",
"ui-card-error",
"ui-form-surface",
"ui-table",
"ui-chip-primary",
"ui-badge-secondary",
"ui-status",
):
assert f".{class_name}" in theme_css