Files
transcription/tests/ui/test_documents_page.py
T

347 lines
12 KiB
Python

"""Tests for the documents page routes and action handlers."""
import re
from datetime import date
import pytest
import pytest_asyncio
from sqlmodel import select
from transcription.db import session_scope
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentTag
from transcription.db.models import DocumentType
from transcription.db.models import Job
from transcription.db.models import Person
from transcription.db.models import PersonRole
from transcription.db.models import Source
from transcription.db.models import Tag
from transcription.ui.pages.documents_page import _first_source_path
from transcription.ui.pages.documents_page import _resolve_selected_tag_labels
# --- Helper Fixtures ---
@pytest_asyncio.fixture
async def seed_person_and_document():
"""Seed a Person and Document linked by DocumentPerson role."""
async with session_scope() as session:
letter_type = (await session.exec(select(DocumentType).where(DocumentType.label == "Letter"))).one()
author_role = (await session.exec(select(PersonRole).where(PersonRole.semantic_key == "author"))).one()
person = Person(given_names="Zenna", last_name="Cochran")
session.add(person)
await session.flush()
doc = Document(
name="Letter from Hig",
document_type_id=letter_type.id,
archive_identifier="ZC-1924-001",
document_date=date(1924, 7, 4),
location_created="Salt Lake City, Utah",
)
session.add(doc)
await session.flush()
link = DocumentPerson(
document_id=doc.id,
person_id=person.id,
role_id=author_role.id,
)
session.add(link)
await session.commit()
return str(doc.id), str(person.id)
# --- Integration Tests for Documents Route Handlers ---
@pytest.mark.integration
class TestDocumentsPageRendering:
"""Verify document list, detail, edit, and deletion route behaviors."""
def test_documents_page_renders_empty_state(self, app_client):
_, client = app_client
response = client.get("/ui/documents")
assert response.status_code == 200
assert "Archival Documents" in response.text
assert "No documents found in repository." in response.text
@pytest.mark.asyncio
async def test_documents_page_lists_seeded_documents(self, app_client):
_, client = app_client
async with session_scope() as session:
postcard_type = (await session.exec(select(DocumentType).where(DocumentType.label == "Postcard"))).one()
family_tag = Tag(label="Family", normalized_label="family")
doc = Document(
name="1924 Postcard",
document_type_id=postcard_type.id,
archive_identifier="PC-001",
)
session.add_all([doc, family_tag])
await session.flush()
session.add(DocumentTag(document_id=doc.id, tag_id=family_tag.id))
await session.commit()
response = client.get("/ui/documents")
assert response.status_code == 200
assert "1924 Postcard" in response.text
assert "Postcard" in response.text
assert "Document Date" in response.text
assert "Author" in response.text
assert "Tags" in response.text
assert "Family" in response.text
assert "# Sources" in response.text
assert "Archive Ref" not in response.text
assert re.search(
r'"name":"name","label":"Document Title".*'
r'"name":"authors","label":"Author".*'
r'"name":"tags","label":"Tags".*'
r'"name":"document_date","label":"Document Date".*'
r'"name":"document_type","label":"Type".*'
r'"name":"source_count","label":"# Sources".*'
r'"name":"transcription_status","label":"Transcription Status"',
response.text,
re.DOTALL,
)
def test_first_source_path_prefers_lowest_page_number(self):
document = Document(name="Ordering Test")
document.sources = [
Source(
document_id=document.id,
page_number=3,
upload_name="c.jpg",
filename="c.jpg",
file_path="documents/c.jpg",
file_hash="c" * 64,
file_size_bytes=1,
),
Source(
document_id=document.id,
page_number=1,
upload_name="a.jpg",
filename="a.jpg",
file_path="documents/a.jpg",
file_hash="a" * 64,
file_size_bytes=1,
),
Source(
document_id=document.id,
page_number=2,
upload_name="b.jpg",
filename="b.jpg",
file_path="documents/b.jpg",
file_hash="b" * 64,
file_size_bytes=1,
),
]
assert _first_source_path(document) == "documents/a.jpg"
def test_document_create_page_renders_form(self, app_client):
_, client = app_client
response = client.get("/ui/documents/new")
assert response.status_code == 200
assert "Create Document" in response.text
assert "Document name" in response.text
assert "Linked People" in response.text
assert "Document type" in response.text
assert "Tags" in response.text
@pytest.mark.asyncio
async def test_document_create_page_preselects_person_with_disambiguating_label(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(
given_names="Albert Edward",
last_name="Higgins",
birth_date=date(1885, 1, 2),
)
session.add(person)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/documents/new?person_id={person_id}")
assert response.status_code == 200
assert "Albert Edward Higgins (1885)" in response.text
@pytest.mark.asyncio
async def test_document_detail_page_renders_bento_grid_and_metadata(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, _ = seed_person_and_document
response = client.get(f"/ui/documents/{doc_id}")
assert response.status_code == 200
assert "Letter from Hig" in response.text
assert "Zenna Cochran" in response.text
assert "Document Details" in response.text
assert "Edit Document" in response.text
assert "Back to Documents" in response.text
@pytest.mark.asyncio
async def test_document_info_page_renders_metadata_cards(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, _ = seed_person_and_document
response = client.get(f"/ui/documents/{doc_id}/info")
assert response.status_code == 200
assert "Document Info" in response.text
assert "ZC-1924-001" in response.text
assert "google.com/maps/search/?api=1&query=Salt+Lake+City%2C+Utah" in response.text
@pytest.mark.asyncio
async def test_document_detail_page_renders_person_context_back_button(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, person_id = seed_person_and_document
response = client.get(f"/ui/documents/{doc_id}?from=person&person_id={person_id}")
assert response.status_code == 200
assert "Back to Person" in response.text
@pytest.mark.asyncio
async def test_document_detail_page_renders_job_context_back_button(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="Job-linked Document")
session.add(doc)
await session.flush()
job = Job(document_id=doc.id)
session.add(job)
await session.commit()
doc_id = str(doc.id)
job_id = str(job.id)
response = client.get(f"/ui/documents/{doc_id}?from=job&job_id={job_id}")
assert response.status_code == 200
assert "Back to Job" in response.text
@pytest.mark.asyncio
async def test_document_jobs_page_redirects_to_filtered_jobs(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="Doc With Job")
session.add(doc)
await session.flush()
job = Job(document_id=doc.id)
session.add(job)
await session.commit()
doc_id = str(doc.id)
_ = str(job.id)
response = client.get(f"/ui/documents/{doc_id}/jobs")
assert response.status_code == 200
assert "Jobs for Document" in response.text
assert "Create job" not in response.text
assert "Refresh" not in response.text
@pytest.mark.asyncio
async def test_document_sources_page_renders_thumbnail_gallery(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="Doc With Sources")
session.add(doc)
await session.flush()
source = Source(
document_id=doc.id,
page_number=1,
upload_name="scan-01.jpg",
filename="scan-01.jpg",
file_path="documents/sample/scan-01.jpg",
file_hash="c" * 64,
file_size_bytes=1,
)
session.add(source)
await session.commit()
doc_id = str(doc.id)
response = client.get(f"/ui/documents/{doc_id}/sources")
assert response.status_code == 200
assert "Source Images" in response.text
assert "Open Source Detail" in response.text
assert "scan-01.jpg" in response.text
@pytest.mark.asyncio
async def test_document_edit_page_prefills_existing_values(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, _ = seed_person_and_document
response = client.get(f"/ui/documents/{doc_id}/edit")
assert response.status_code == 200
assert "Edit Document Record" in response.text
assert "Letter from Hig" in response.text
assert "Document date" in response.text
assert "Exact date (YYYY-MM-DD)" not in response.text
assert "ZC-1924-001" in response.text
@pytest.mark.asyncio
async def test_document_delete_page_blocks_deletion_when_dependencies_exist(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="Doc With Source")
session.add(doc)
await session.flush()
source = Source(
document_id=doc.id,
page_number=1,
upload_name="page_1.png",
filename="page_1.png",
file_path="/tmp/page_1.png",
file_hash="0" * 64,
file_size_bytes=1,
)
session.add(source)
await session.commit()
doc_id = str(doc.id)
response = client.get(f"/ui/documents/{doc_id}/delete")
assert response.status_code == 200
assert "Delete Document" in response.text
assert "Delete is blocked because related records exist." in response.text
assert "Dependencies present: Sources" in response.text
@pytest.mark.asyncio
async def test_document_delete_page_allows_unlinked_document_deletion(self, app_client):
_, client = app_client
async with session_scope() as session:
doc = Document(name="Orphan Document")
session.add(doc)
await session.commit()
doc_id = str(doc.id)
response = client.get(f"/ui/documents/{doc_id}/delete")
assert response.status_code == 200
assert "Delete Document" in response.text
assert "Delete document permanently" in response.text
assert "Delete is blocked" not in response.text
def test_resolve_selected_tag_labels_handles_multiple_payload_shapes():
assert _resolve_selected_tag_labels("Family") == ["Family"]
assert _resolve_selected_tag_labels(["Family", "Research"]) == ["Family", "Research"]
assert _resolve_selected_tag_labels([{"label": "Family"}, {"value": "Research"}]) == ["Family", "Research"]
assert set(_resolve_selected_tag_labels({"value": {"Family", "Research"}})) == {"Family", "Research"}