Files
transcription/tests/ui/test_people_page.py
T
2026-08-31 11:31:25 -05:00

382 lines
15 KiB
Python

"""Tests for the people page routes and action handlers."""
from datetime import date
from uuid import uuid4
import pytest
from sqlmodel import select
from transcription.config import get_settings
from transcription.db import session_scope
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import Person
from transcription.db.models import PersonRole
from transcription.db.models import PersonTag
from transcription.db.models import Photo
from transcription.db.models import Source
from transcription.db.models import Tag
@pytest.mark.integration
class TestPeoplePageRendering:
"""Verify people routes render expected CRUD read states."""
def test_people_page_renders_empty_state(self, app_client):
_, client = app_client
response = client.get("/ui/people")
assert response.status_code == 200
assert "People" in response.text
assert "Create new person" in response.text
assert "No person records found in repository." in response.text
@pytest.mark.asyncio
async def test_people_page_lists_seeded_people(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(given_names="Ada", last_name="Lovelace")
tag = Tag(label="Pioneer", normalized_label="pioneer")
session.add_all([person, tag])
await session.flush()
session.add(PersonTag(person_id=person.id, tag_id=tag.id))
await session.commit()
response = client.get("/ui/people")
assert response.status_code == 200
assert "Lovelace, Ada" in response.text
assert "Last Name, First & Middle" in response.text or "Last Name, First & Middle" in response.text
assert "Tags" in response.text
assert "Pioneer" in response.text
assert "FamilySearch ID" in response.text
assert "# Documents" in response.text
@pytest.mark.asyncio
async def test_people_page_shows_document_counts(self, app_client):
_, client = app_client
async with session_scope() as session:
role = (await session.exec(select(PersonRole).where(PersonRole.semantic_key == "author"))).one()
person = Person(given_names="Counted", last_name="Person")
document = Document(name="Linked For Count")
session.add_all([person, document])
await session.flush()
session.add(DocumentPerson(document_id=document.id, person_id=person.id, role_id=role.id))
await session.commit()
response = client.get("/ui/people")
assert response.status_code == 200
assert '"document_count":1' in response.text
def test_person_create_page_renders_fields(self, app_client):
_, client = app_client
response = client.get("/ui/people/new")
assert response.status_code == 200
assert "Create Person Record" in response.text
assert "Last name and first/middle names are required." in response.text
assert "Birth date" in response.text
assert "Death date" in response.text
assert "Birth date (YYYY-MM-DD)" not in response.text
assert "Death date (YYYY-MM-DD)" not in response.text
assert "FamilySearch ID" in response.text
assert "Auto-fill from FamilySearch" not in response.text
assert "Marriage date (FamilySearch)" not in response.text
assert "Spouse (FamilySearch)" not in response.text
assert "Biography" in response.text
assert "Save person" in response.text
@pytest.mark.asyncio
async def test_person_detail_page_renders_metadata_and_empty_links(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(
given_names="Grace",
last_name="Hopper",
birth_date=date(1906, 12, 9),
birth_date_raw="1906",
birth_place="New York",
death_date=date(1992, 1, 1),
death_date_raw="1992",
death_place="Arlington",
biography="Computer pioneer",
family_search_id="G8T4-MDQ",
)
session.add(person)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}")
assert response.status_code == 200
assert "Grace Hopper" in response.text
assert "Full Name:" in response.text
assert "Last Name:" in response.text
assert "First & Middle:" in response.text or "First & Middle:" in response.text
assert "Birth Date:" in response.text
assert "1906-12-09" in response.text
assert "Death Date:" in response.text
assert "1992-01-01" in response.text
assert "Open Birth Place in Google Maps" not in response.text
assert "Open Death Place in Google Maps" not in response.text
assert "google.com/maps/search/?api=1&query=New+York" in response.text
assert "google.com/maps/search/?api=1&query=Arlington" in response.text
assert "biography" in response.text.lower()
assert "Computer pioneer" in response.text
assert "Created:" in response.text
assert "Updated:" in response.text
assert "Open in FamilySearch" not in response.text
assert "FamilySearch ID:" in response.text
assert "familysearch.org/tree/person/details/G8T4-MDQ" in response.text
assert "Back to People" in response.text
assert "New Document" in response.text
assert "No linked documents yet." in response.text
@pytest.mark.asyncio
async def test_person_detail_page_renders_document_context_back_button(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(given_names="Context", last_name="Person")
document = Document(name="Context Document")
session.add_all([person, document])
await session.commit()
person_id = str(person.id)
document_id = str(document.id)
response = client.get(f"/ui/people/{person_id}?from=document&document_id={document_id}")
assert response.status_code == 200
assert "Back to Document" in response.text
@pytest.mark.asyncio
async def test_person_detail_page_resolves_relative_photo_path(self, app_client):
app, client = app_client
upload_dirs = {app.state.settings.upload_dir, get_settings().upload_dir}
for upload_dir in upload_dirs:
photo_file = upload_dir / "photos" / "seeded.png"
photo_file.parent.mkdir(parents=True, exist_ok=True)
photo_file.write_bytes(b"portrait")
async with session_scope() as session:
person = Person(given_names="Portrait", last_name="Person")
session.add(person)
await session.flush()
session.add(
Photo(
person_id=person.id,
path="photos/seeded.png",
is_primary=True,
)
)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}")
assert response.status_code == 200
assert "No source media available for inspection." not in response.text
assert "Edit Photo(s)" in response.text
assert response.text.count("Edit Photo(s)") == 1
assert "Upload photo(s)" not in response.text
@pytest.mark.asyncio
async def test_person_detail_page_shows_gallery_navigation_for_multiple_photos(self, app_client):
app, client = app_client
upload_dirs = {app.state.settings.upload_dir, get_settings().upload_dir}
for upload_dir in upload_dirs:
primary_file = upload_dir / "photos" / "portrait-primary.png"
secondary_file = upload_dir / "photos" / "portrait-secondary.png"
primary_file.parent.mkdir(parents=True, exist_ok=True)
primary_file.write_bytes(b"portrait-primary")
secondary_file.write_bytes(b"portrait-secondary")
async with session_scope() as session:
person = Person(given_names="Gallery", last_name="Navigation")
session.add(person)
await session.flush()
session.add_all(
[
Photo(
person_id=person.id,
path="photos/portrait-primary.png",
is_primary=True,
),
Photo(
person_id=person.id,
path="photos/portrait-secondary.png",
is_primary=False,
),
]
)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}")
assert response.status_code == 200
assert "Previous" in response.text
assert "Next" in response.text
assert "1 of 2" in response.text
@pytest.mark.asyncio
async def test_person_photos_page_renders_photo_management_controls(self, app_client):
app, client = app_client
upload_dirs = {app.state.settings.upload_dir, get_settings().upload_dir}
for upload_dir in upload_dirs:
photo_file = upload_dir / "photos" / "seeded.png"
photo_file.parent.mkdir(parents=True, exist_ok=True)
photo_file.write_bytes(b"portrait")
async with session_scope() as session:
person = Person(given_names="Gallery", last_name="Person")
session.add(person)
await session.flush()
session.add(
Photo(
person_id=person.id,
path="photos/seeded.png",
is_primary=True,
description="Seeded description",
)
)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}/photos")
assert response.status_code == 200
assert "Edit Photos" in response.text
assert "Upload Photo(s)" in response.text
assert "Back to Person" in response.text
assert "Save Description" in response.text
assert "Delete Photo" in response.text
assert "Set Primary" not in response.text
@pytest.mark.asyncio
async def test_person_detail_page_renders_linked_documents(self, app_client):
_, client = app_client
async with session_scope() as session:
author_role = (await session.exec(select(PersonRole).where(PersonRole.semantic_key == "author"))).one()
person = Person(given_names="Linked", last_name="Person")
document = Document(name="Linked Document")
session.add_all([person, document])
await session.flush()
session.add_all(
[
Source(
document_id=document.id,
page_number=1,
upload_name="linked-page-1.png",
filename="linked-page-1.png",
file_path="/tmp/linked-page-1.png",
file_hash="1" * 64,
file_size_bytes=1,
),
Source(
document_id=document.id,
page_number=2,
upload_name="linked-page-2.png",
filename="linked-page-2.png",
file_path="/tmp/linked-page-2.png",
file_hash="2" * 64,
file_size_bytes=1,
),
]
)
session.add(
DocumentPerson(
document_id=document.id,
person_id=person.id,
role_id=author_role.id,
)
)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}")
assert response.status_code == 200
assert "Document Name" in response.text
assert "Document Date" in response.text
assert "Number of Pages" in response.text
assert "Linked Document" in response.text
assert "Author" in response.text
assert '"page_count":2' in response.text
assert "Open" not in response.text
@pytest.mark.asyncio
async def test_person_detail_page_hides_removed_maiden_name_field(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(given_names="No Maiden", last_name="Name")
session.add(person)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}")
assert response.status_code == 200
assert "Maiden Name:" not in response.text
def test_person_detail_page_handles_invalid_id(self, app_client):
_, client = app_client
response = client.get("/ui/people/not-a-uuid")
assert response.status_code == 200
assert "Invalid person id" in response.text
def test_person_detail_page_handles_missing_person(self, app_client):
_, client = app_client
response = client.get(f"/ui/people/{uuid4()}")
assert response.status_code == 200
assert "Person not found" in response.text
@pytest.mark.asyncio
async def test_person_edit_page_renders_expected_fields(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(given_names="Editable", last_name="Person")
session.add(person)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}/edit")
assert response.status_code == 200
assert "Edit Person Record" in response.text
assert "Last name and first/middle names are required." in response.text
assert "Editable" in response.text
assert "Person" in response.text
assert "Save changes" in response.text
@pytest.mark.asyncio
async def test_person_delete_page_shows_confirmation_when_unlinked(self, app_client):
_, client = app_client
async with session_scope() as session:
person = Person(given_names="Safe", last_name="Delete")
session.add(person)
await session.commit()
person_id = str(person.id)
response = client.get(f"/ui/people/{person_id}/delete")
assert response.status_code == 200
assert "Delete Person Record" in response.text
assert "This action permanently deletes the person record." in response.text
assert "Delete person permanently" in response.text