generated from john/python-template
347 lines
12 KiB
Python
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"}
|