Files

414 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests for in-browser document preview functionality.
Tests the pdf.js-based PDF viewer, image zoom/pan, text preview with
line numbers, and the file-list preview modal introduced by the
in-browser document preview feature.
"""
import os
import pytest
from fastapi.testclient import TestClient
from app.models import FileRecord
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _create_file_record(
db_session,
*,
filename: str = "test.pdf",
mime_type: str = "application/pdf",
file_path: str,
processed_path: str | None = None,
) -> FileRecord:
"""Create and persist a FileRecord for testing."""
rec = FileRecord(
filehash="hash_" + filename,
original_filename=filename,
local_filename=file_path,
original_file_path=file_path,
processed_file_path=processed_path,
file_size=os.path.getsize(file_path) if os.path.exists(file_path) else 1024,
mime_type=mime_type,
)
db_session.add(rec)
db_session.commit()
db_session.refresh(rec)
return rec
# ===========================================================================
# file_view.html PDF preview via pdf.js
# ===========================================================================
@pytest.mark.unit
class TestFileViewPdfJs:
"""file_view.html should embed a pdf.js viewer for PDF files."""
def test_pdf_file_renders_pdfjs_viewer(self, client: TestClient, db_session, tmp_path):
"""PDF file view should include pdf.js library and canvas-based viewer."""
pdf = tmp_path / "doc.pdf"
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
# pdf.js library must be loaded
assert "pdf.min.js" in html
assert "pdf.worker.min.js" in html
# pdf.js canvas wrapper and page controls must be present
assert 'id="pdf-viewer"' in html
assert 'id="pdf-canvas-wrap"' in html
assert 'id="pdf-page-info"' in html
assert "pdfInit(" in html
# Should NOT have an iframe in the preview section
assert 'id="pdf-viewer"' in html
def test_pdf_file_no_iframe(self, client: TestClient, db_session, tmp_path):
"""PDF should not use native iframe-based preview."""
pdf = tmp_path / "doc2.pdf"
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# The preview section should use pdf-viewer, not iframe
assert 'id="pdf-viewer"' in html
assert "pdf-canvas-wrap" in html
def test_page_navigation_controls(self, client: TestClient, db_session, tmp_path):
"""PDF viewer should have previous/next page buttons."""
pdf = tmp_path / "nav.pdf"
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert 'id="pdf-prev-btn"' in html
assert 'id="pdf-next-btn"' in html
assert "pdfChangePage" in html
# ===========================================================================
# file_view.html Image preview with zoom / pan
# ===========================================================================
@pytest.mark.unit
class TestFileViewImagePreview:
"""file_view.html should show an image viewer with zoom/pan for image files."""
def test_image_file_renders_image_viewer(self, client: TestClient, db_session, tmp_path):
"""Image files should show zoom/pan controls instead of iframe."""
img = tmp_path / "photo.jpg"
img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100) # minimal JPEG header
rec = _create_file_record(db_session, filename="photo.jpg", mime_type="image/jpeg", file_path=str(img))
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
assert 'id="img-viewer"' in html
assert 'id="preview-img"' in html
assert 'id="img-wrap"' in html
assert "imgZoom" in html
assert "imgReset" in html
def test_image_zoom_controls(self, client: TestClient, db_session, tmp_path):
"""Image viewer should have zoom in, zoom out, and reset controls."""
img = tmp_path / "photo.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
rec = _create_file_record(db_session, filename="photo.png", mime_type="image/png", file_path=str(img))
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert 'aria-label="Zoom in"' in html
assert 'aria-label="Zoom out"' in html
assert 'aria-label="Reset zoom"' in html
assert 'id="img-zoom-level"' in html
def test_image_pan_support(self, client: TestClient, db_session, tmp_path):
"""Image viewer should support mouse-drag panning."""
img = tmp_path / "wide.webp"
img.write_bytes(b"RIFF" + b"\x00" * 50)
rec = _create_file_record(db_session, filename="wide.webp", mime_type="image/webp", file_path=str(img))
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# Pan support is implemented via JavaScript on img-wrap
assert "mousedown" in html
assert "mousemove" in html
assert "cursor: grab" in html or "cursor:grab" in html
# ===========================================================================
# file_view.html Text file preview with line numbers
# ===========================================================================
@pytest.mark.unit
class TestFileViewTextPreview:
"""file_view.html should show a text viewer with line numbers for text files."""
def test_text_file_renders_text_viewer(self, client: TestClient, db_session, tmp_path):
"""Text files should show the text-viewer component."""
txt = tmp_path / "readme.txt"
txt.write_text("Hello world\nSecond line\n")
rec = _create_file_record(db_session, filename="readme.txt", mime_type="text/plain", file_path=str(txt))
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
assert 'id="text-viewer"' in html
assert 'id="text-preview-content"' in html
assert "loadTextPreview" in html
def test_text_viewer_has_copy_button(self, client: TestClient, db_session, tmp_path):
"""Text viewer should have a Copy button."""
txt = tmp_path / "code.py"
txt.write_text("print('hello')\n")
rec = _create_file_record(db_session, filename="code.py", mime_type="text/x-python", file_path=str(txt))
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert "copyTextPreview" in html
assert 'id="text-copy-btn"' in html
def test_text_viewer_line_numbers(self, client: TestClient, db_session, tmp_path):
"""Text viewer JS should generate line numbers."""
txt = tmp_path / "data.csv"
txt.write_text("a,b,c\n1,2,3\n")
rec = _create_file_record(db_session, filename="data.csv", mime_type="text/csv", file_path=str(txt))
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# JS builds line-number spans
assert "line-number" in html
# ===========================================================================
# file_view.html Preview icon adapts to MIME type
# ===========================================================================
@pytest.mark.unit
class TestFileViewPreviewIcon:
"""Preview card icon should match the MIME type of the file."""
def test_pdf_icon(self, client: TestClient, db_session, tmp_path):
"""PDF files show a file-pdf icon."""
pdf = tmp_path / "a.pdf"
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-file-pdf" in html
def test_image_icon(self, client: TestClient, db_session, tmp_path):
"""Image files show an image icon."""
img = tmp_path / "p.jpg"
img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 10)
rec = _create_file_record(db_session, filename="p.jpg", mime_type="image/jpeg", file_path=str(img))
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-image" in html
def test_text_icon(self, client: TestClient, db_session, tmp_path):
"""Text files show a code icon."""
txt = tmp_path / "t.txt"
txt.write_text("hello")
rec = _create_file_record(db_session, filename="t.txt", mime_type="text/plain", file_path=str(txt))
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-file-code" in html
# ===========================================================================
# files.html Preview modal from file list
# ===========================================================================
@pytest.mark.unit
class TestFilesPreviewModal:
"""files.html should include a preview side-panel triggered from the file list."""
def test_preview_modal_markup(self, client: TestClient, db_session, tmp_path):
"""Files page should contain the preview overlay HTML."""
pdf = tmp_path / "list.pdf"
pdf.write_bytes(b"%PDF-1.4")
_create_file_record(db_session, file_path=str(pdf))
response = client.get("/files")
assert response.status_code == 200
html = response.text
assert 'id="previewOverlay"' in html
assert 'id="previewBody"' in html
assert 'id="previewTitle"' in html
assert 'role="dialog"' in html
assert 'aria-modal="true"' in html
def test_preview_button_in_actions(self, client: TestClient, db_session, tmp_path):
"""Each file row should have a preview (eye) button."""
pdf = tmp_path / "btn.pdf"
pdf.write_bytes(b"%PDF-1.4")
_create_file_record(db_session, file_path=str(pdf))
html = client.get("/files").text
assert "openPreviewModal" in html
assert "fa-eye" in html
def test_preview_modal_has_pdfjs(self, client: TestClient, db_session, tmp_path):
"""Files page should load pdf.js for the preview modal."""
pdf = tmp_path / "js.pdf"
pdf.write_bytes(b"%PDF-1.4")
_create_file_record(db_session, file_path=str(pdf))
html = client.get("/files").text
assert "pdf.min.js" in html
assert "pdfjsLib" in html
def test_preview_modal_close(self, client: TestClient, db_session, tmp_path):
"""Preview modal should have a close button and Escape handler."""
pdf = tmp_path / "close.pdf"
pdf.write_bytes(b"%PDF-1.4")
_create_file_record(db_session, file_path=str(pdf))
html = client.get("/files").text
assert "closePreviewModal" in html
assert "Escape" in html
def test_preview_modal_footer_links(self, client: TestClient, db_session, tmp_path):
"""Preview modal JS should include download and full-view links."""
pdf = tmp_path / "footer.pdf"
pdf.write_bytes(b"%PDF-1.4")
_create_file_record(db_session, file_path=str(pdf))
html = client.get("/files").text
assert "Full view" in html
assert "Download" in html
# ===========================================================================
# file_detail.html Bottom preview section uses correct variable names
# ===========================================================================
@pytest.mark.unit
class TestFileDetailBottomPreview:
"""file_detail.html bottom File Previews card should render when files exist."""
def test_bottom_preview_card_renders(self, client: TestClient, db_session, tmp_path):
"""Bottom 'File Previews' card should display when file exists on disk."""
pdf = tmp_path / "detail.pdf"
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf))
response = client.get(f"/files/{rec.id}/process")
assert response.status_code == 200
html = response.text
# The fixed section should now render (was previously using wrong variable names)
assert "File Previews" in html
assert "Original File" in html or "Processed File" in html
def test_bottom_preview_has_download_link(self, client: TestClient, db_session, tmp_path):
"""Bottom preview card should have download links."""
pdf = tmp_path / "dl.pdf"
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf))
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert f"/api/files/{rec.id}/download" in html
def test_image_preview_in_detail(self, client: TestClient, db_session, tmp_path):
"""Image files should render an <img> tag in the bottom preview card."""
img = tmp_path / "detail_img.jpg"
img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100)
rec = _create_file_record(
db_session,
filename="detail_img.jpg",
mime_type="image/jpeg",
file_path=str(img),
)
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert f"/api/files/{rec.id}/preview?version=original" in html
# ===========================================================================
# Existing OCR text functionality preserved
# ===========================================================================
@pytest.mark.unit
class TestFileViewOcrText:
"""Verify that the existing OCR text feature still works after the preview upgrade."""
def test_ocr_text_toggle_present(self, client: TestClient, db_session, tmp_path):
"""Pages with ocr_text should still have the toggle button."""
pdf = tmp_path / "ocr.pdf"
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf))
rec.ocr_text = "Sample extracted OCR text content"
db_session.commit()
html = client.get(f"/files/{rec.id}/detail").text
assert "toggleOcrText" in html
assert "ocr-text-block" in html
assert "Sample extracted OCR text content" in html
def test_text_extraction_button(self, client: TestClient, db_session, tmp_path):
"""Pages without ocr_text but with files should show extract button."""
pdf = tmp_path / "no_ocr.pdf"
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf))
html = client.get(f"/files/{rec.id}/detail").text
assert "loadText" in html or "Extract" in html
# ===========================================================================
# No file available fallback
# ===========================================================================
@pytest.mark.unit
class TestFileViewNoFile:
"""When no file is on disk, a fallback message should display."""
def test_no_preview_fallback(self, client: TestClient, db_session):
"""No-file-on-disk scenario shows a fallback message."""
rec = FileRecord(
filehash="nope",
original_filename="gone.pdf",
local_filename="/nonexistent/gone.pdf",
file_size=0,
mime_type="application/pdf",
)
db_session.add(rec)
db_session.commit()
db_session.refresh(rec)
html = client.get(f"/files/{rec.id}/detail").text
assert "No file available for preview" in html