Add a stronger testing strategy
This commit is contained in:
parent
ac1b68ac18
commit
049ef443cc
16 changed files with 1179 additions and 19 deletions
|
|
@ -1,19 +0,0 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-docling_studio}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-docling_studio}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
4
document-parser/conftest.py
Normal file
4
document-parser/conftest.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import pytest
|
||||
|
||||
|
||||
pytest_plugins = ["pytest_asyncio"]
|
||||
2
document-parser/pytest.ini
Normal file
2
document-parser/pytest.ini
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
asyncio_mode = auto
|
||||
0
document-parser/tests/__init__.py
Normal file
0
document-parser/tests/__init__.py
Normal file
178
document-parser/tests/test_api_endpoints.py
Normal file
178
document-parser/tests/test_api_endpoints.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""Tests for FastAPI API endpoints using TestClient."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from domain.models import AnalysisJob, AnalysisStatus, Document
|
||||
from main import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
class TestHealthEndpoint:
|
||||
def test_health(self, client):
|
||||
resp = client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
|
||||
|
||||
class TestDocumentEndpoints:
|
||||
@patch("services.document_service.find_all", new_callable=AsyncMock)
|
||||
def test_list_documents(self, mock_find_all, client):
|
||||
mock_find_all.return_value = [
|
||||
Document(id="d1", filename="a.pdf", storage_path="/tmp/a"),
|
||||
Document(id="d2", filename="b.pdf", storage_path="/tmp/b"),
|
||||
]
|
||||
|
||||
resp = client.get("/api/documents")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 2
|
||||
assert data[0]["id"] == "d1"
|
||||
assert data[0]["filename"] == "a.pdf"
|
||||
# Verify camelCase
|
||||
assert "createdAt" in data[0]
|
||||
|
||||
@patch("services.document_service.find_by_id", new_callable=AsyncMock)
|
||||
def test_get_document(self, mock_find, client):
|
||||
mock_find.return_value = Document(
|
||||
id="d1", filename="test.pdf", content_type="application/pdf",
|
||||
file_size=2048, page_count=3, storage_path="/tmp/test",
|
||||
)
|
||||
|
||||
resp = client.get("/api/documents/d1")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["id"] == "d1"
|
||||
assert data["fileSize"] == 2048
|
||||
assert data["pageCount"] == 3
|
||||
|
||||
@patch("services.document_service.find_by_id", new_callable=AsyncMock)
|
||||
def test_get_document_not_found(self, mock_find, client):
|
||||
mock_find.return_value = None
|
||||
|
||||
resp = client.get("/api/documents/missing")
|
||||
assert resp.status_code == 404
|
||||
|
||||
@patch("services.document_service.upload", new_callable=AsyncMock)
|
||||
def test_upload_document(self, mock_upload, client):
|
||||
mock_upload.return_value = Document(
|
||||
id="new-1", filename="uploaded.pdf",
|
||||
content_type="application/pdf", file_size=512,
|
||||
storage_path="/tmp/uploaded",
|
||||
)
|
||||
|
||||
resp = client.post(
|
||||
"/api/documents/upload",
|
||||
files={"file": ("uploaded.pdf", b"fake-pdf-content", "application/pdf")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["id"] == "new-1"
|
||||
assert data["filename"] == "uploaded.pdf"
|
||||
|
||||
@patch("services.document_service.upload", new_callable=AsyncMock)
|
||||
def test_upload_too_large(self, mock_upload, client):
|
||||
mock_upload.side_effect = ValueError("File too large (max 50 MB)")
|
||||
|
||||
resp = client.post(
|
||||
"/api/documents/upload",
|
||||
files={"file": ("big.pdf", b"x", "application/pdf")},
|
||||
)
|
||||
assert resp.status_code == 413
|
||||
|
||||
@patch("services.document_service.delete", new_callable=AsyncMock)
|
||||
def test_delete_document(self, mock_delete, client):
|
||||
mock_delete.return_value = True
|
||||
|
||||
resp = client.delete("/api/documents/d1")
|
||||
assert resp.status_code == 204
|
||||
|
||||
@patch("services.document_service.delete", new_callable=AsyncMock)
|
||||
def test_delete_document_not_found(self, mock_delete, client):
|
||||
mock_delete.return_value = False
|
||||
|
||||
resp = client.delete("/api/documents/missing")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestAnalysisEndpoints:
|
||||
@patch("services.analysis_service.find_all", new_callable=AsyncMock)
|
||||
def test_list_analyses(self, mock_find_all, client):
|
||||
mock_find_all.return_value = [
|
||||
AnalysisJob(id="j1", document_id="d1", document_filename="test.pdf"),
|
||||
]
|
||||
|
||||
resp = client.get("/api/analyses")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == "j1"
|
||||
assert data[0]["documentId"] == "d1"
|
||||
assert data[0]["documentFilename"] == "test.pdf"
|
||||
assert data[0]["status"] == "PENDING"
|
||||
|
||||
@patch("services.analysis_service.find_by_id", new_callable=AsyncMock)
|
||||
def test_get_analysis(self, mock_find, client):
|
||||
job = AnalysisJob(id="j1", document_id="d1", document_filename="test.pdf")
|
||||
job.mark_running()
|
||||
mock_find.return_value = job
|
||||
|
||||
resp = client.get("/api/analyses/j1")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "RUNNING"
|
||||
assert data["startedAt"] is not None
|
||||
|
||||
@patch("services.analysis_service.find_by_id", new_callable=AsyncMock)
|
||||
def test_get_analysis_not_found(self, mock_find, client):
|
||||
mock_find.return_value = None
|
||||
|
||||
resp = client.get("/api/analyses/missing")
|
||||
assert resp.status_code == 404
|
||||
|
||||
@patch("services.analysis_service.create", new_callable=AsyncMock)
|
||||
def test_create_analysis(self, mock_create, client):
|
||||
mock_create.return_value = AnalysisJob(
|
||||
id="j1", document_id="d1", document_filename="test.pdf",
|
||||
)
|
||||
|
||||
resp = client.post("/api/analyses", json={"documentId": "d1"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["id"] == "j1"
|
||||
assert data["documentId"] == "d1"
|
||||
|
||||
@patch("services.analysis_service.create", new_callable=AsyncMock)
|
||||
def test_create_analysis_document_not_found(self, mock_create, client):
|
||||
mock_create.side_effect = ValueError("Document not found: d99")
|
||||
|
||||
resp = client.post("/api/analyses", json={"documentId": "d99"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_create_analysis_empty_document_id(self, client):
|
||||
resp = client.post("/api/analyses", json={"documentId": ""})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_create_analysis_whitespace_document_id(self, client):
|
||||
resp = client.post("/api/analyses", json={"documentId": " "})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("services.analysis_service.delete", new_callable=AsyncMock)
|
||||
def test_delete_analysis(self, mock_delete, client):
|
||||
mock_delete.return_value = True
|
||||
|
||||
resp = client.delete("/api/analyses/j1")
|
||||
assert resp.status_code == 204
|
||||
|
||||
@patch("services.analysis_service.delete", new_callable=AsyncMock)
|
||||
def test_delete_analysis_not_found(self, mock_delete, client):
|
||||
mock_delete.return_value = False
|
||||
|
||||
resp = client.delete("/api/analyses/missing")
|
||||
assert resp.status_code == 404
|
||||
108
document-parser/tests/test_models.py
Normal file
108
document-parser/tests/test_models.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""Tests for domain models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from domain.models import AnalysisJob, AnalysisStatus, Document
|
||||
|
||||
|
||||
class TestDocument:
|
||||
def test_default_values(self):
|
||||
doc = Document()
|
||||
assert doc.id # auto-generated UUID
|
||||
assert doc.filename == ""
|
||||
assert doc.content_type is None
|
||||
assert doc.file_size is None
|
||||
assert doc.page_count is None
|
||||
assert doc.storage_path == ""
|
||||
assert isinstance(doc.created_at, datetime)
|
||||
|
||||
def test_custom_values(self):
|
||||
doc = Document(
|
||||
id="doc-1",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
file_size=1024,
|
||||
page_count=5,
|
||||
storage_path="/tmp/test.pdf",
|
||||
)
|
||||
assert doc.id == "doc-1"
|
||||
assert doc.filename == "test.pdf"
|
||||
assert doc.file_size == 1024
|
||||
assert doc.page_count == 5
|
||||
|
||||
def test_unique_ids(self):
|
||||
d1 = Document()
|
||||
d2 = Document()
|
||||
assert d1.id != d2.id
|
||||
|
||||
|
||||
class TestAnalysisJob:
|
||||
def test_default_values(self):
|
||||
job = AnalysisJob()
|
||||
assert job.id
|
||||
assert job.document_id == ""
|
||||
assert job.status == AnalysisStatus.PENDING
|
||||
assert job.content_markdown is None
|
||||
assert job.content_html is None
|
||||
assert job.pages_json is None
|
||||
assert job.error_message is None
|
||||
assert job.started_at is None
|
||||
assert job.completed_at is None
|
||||
|
||||
def test_mark_running(self):
|
||||
job = AnalysisJob()
|
||||
assert job.started_at is None
|
||||
|
||||
job.mark_running()
|
||||
|
||||
assert job.status == AnalysisStatus.RUNNING
|
||||
assert job.started_at is not None
|
||||
|
||||
def test_mark_completed(self):
|
||||
job = AnalysisJob()
|
||||
job.mark_running()
|
||||
|
||||
job.mark_completed(
|
||||
markdown="# Title",
|
||||
html="<h1>Title</h1>",
|
||||
pages_json='[{"page": 1}]',
|
||||
)
|
||||
|
||||
assert job.status == AnalysisStatus.COMPLETED
|
||||
assert job.content_markdown == "# Title"
|
||||
assert job.content_html == "<h1>Title</h1>"
|
||||
assert job.pages_json == '[{"page": 1}]'
|
||||
assert job.completed_at is not None
|
||||
|
||||
def test_mark_failed(self):
|
||||
job = AnalysisJob()
|
||||
job.mark_running()
|
||||
|
||||
job.mark_failed("Something went wrong")
|
||||
|
||||
assert job.status == AnalysisStatus.FAILED
|
||||
assert job.error_message == "Something went wrong"
|
||||
assert job.completed_at is not None
|
||||
|
||||
def test_status_transitions(self):
|
||||
"""Test full lifecycle: PENDING -> RUNNING -> COMPLETED."""
|
||||
job = AnalysisJob()
|
||||
assert job.status == AnalysisStatus.PENDING
|
||||
|
||||
job.mark_running()
|
||||
assert job.status == AnalysisStatus.RUNNING
|
||||
|
||||
job.mark_completed(markdown="md", html="html", pages_json="[]")
|
||||
assert job.status == AnalysisStatus.COMPLETED
|
||||
|
||||
|
||||
class TestAnalysisStatus:
|
||||
def test_values(self):
|
||||
assert AnalysisStatus.PENDING == "PENDING"
|
||||
assert AnalysisStatus.RUNNING == "RUNNING"
|
||||
assert AnalysisStatus.COMPLETED == "COMPLETED"
|
||||
assert AnalysisStatus.FAILED == "FAILED"
|
||||
|
||||
def test_from_string(self):
|
||||
assert AnalysisStatus("PENDING") == AnalysisStatus.PENDING
|
||||
assert AnalysisStatus("COMPLETED") == AnalysisStatus.COMPLETED
|
||||
157
document-parser/tests/test_repos.py
Normal file
157
document-parser/tests/test_repos.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""Tests for persistence repositories using a temporary SQLite database."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.models import AnalysisJob, AnalysisStatus, Document
|
||||
from persistence import document_repo, analysis_repo
|
||||
from persistence.database import init_db
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_db(monkeypatch, tmp_path):
|
||||
"""Use a temp file SQLite database for all repo tests."""
|
||||
db_path = str(tmp_path / "test.db")
|
||||
monkeypatch.setattr("persistence.database.DB_PATH", db_path)
|
||||
await init_db()
|
||||
yield
|
||||
|
||||
|
||||
class TestDocumentRepo:
|
||||
async def test_insert_and_find_by_id(self):
|
||||
doc = Document(
|
||||
id="doc-1",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
file_size=1024,
|
||||
storage_path="/tmp/test.pdf",
|
||||
)
|
||||
await document_repo.insert(doc)
|
||||
|
||||
found = await document_repo.find_by_id("doc-1")
|
||||
assert found is not None
|
||||
assert found.id == "doc-1"
|
||||
assert found.filename == "test.pdf"
|
||||
assert found.file_size == 1024
|
||||
|
||||
async def test_find_by_id_not_found(self):
|
||||
found = await document_repo.find_by_id("nonexistent")
|
||||
assert found is None
|
||||
|
||||
async def test_find_all(self):
|
||||
for i in range(3):
|
||||
doc = Document(id=f"doc-{i}", filename=f"file{i}.pdf", storage_path=f"/tmp/{i}")
|
||||
await document_repo.insert(doc)
|
||||
|
||||
all_docs = await document_repo.find_all()
|
||||
assert len(all_docs) == 3
|
||||
|
||||
async def test_update_page_count(self):
|
||||
doc = Document(id="doc-1", filename="test.pdf", storage_path="/tmp/test.pdf")
|
||||
await document_repo.insert(doc)
|
||||
|
||||
await document_repo.update_page_count("doc-1", 10)
|
||||
|
||||
updated = await document_repo.find_by_id("doc-1")
|
||||
assert updated.page_count == 10
|
||||
|
||||
async def test_delete(self):
|
||||
doc = Document(id="doc-1", filename="test.pdf", storage_path="/tmp/test.pdf")
|
||||
await document_repo.insert(doc)
|
||||
|
||||
deleted = await document_repo.delete("doc-1")
|
||||
assert deleted is True
|
||||
|
||||
found = await document_repo.find_by_id("doc-1")
|
||||
assert found is None
|
||||
|
||||
async def test_delete_nonexistent(self):
|
||||
deleted = await document_repo.delete("nonexistent")
|
||||
assert deleted is False
|
||||
|
||||
|
||||
class TestAnalysisRepo:
|
||||
async def _insert_doc(self):
|
||||
doc = Document(id="doc-1", filename="test.pdf", storage_path="/tmp/test.pdf")
|
||||
await document_repo.insert(doc)
|
||||
return doc
|
||||
|
||||
async def test_insert_and_find_by_id(self):
|
||||
await self._insert_doc()
|
||||
job = AnalysisJob(id="job-1", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
found = await analysis_repo.find_by_id("job-1")
|
||||
assert found is not None
|
||||
assert found.id == "job-1"
|
||||
assert found.document_id == "doc-1"
|
||||
assert found.status == AnalysisStatus.PENDING
|
||||
assert found.document_filename == "test.pdf"
|
||||
|
||||
async def test_find_by_id_not_found(self):
|
||||
found = await analysis_repo.find_by_id("nonexistent")
|
||||
assert found is None
|
||||
|
||||
async def test_find_all(self):
|
||||
await self._insert_doc()
|
||||
for i in range(3):
|
||||
job = AnalysisJob(id=f"job-{i}", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
all_jobs = await analysis_repo.find_all()
|
||||
assert len(all_jobs) == 3
|
||||
|
||||
async def test_update_status(self):
|
||||
await self._insert_doc()
|
||||
job = AnalysisJob(id="job-1", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
job.mark_running()
|
||||
await analysis_repo.update_status(job)
|
||||
|
||||
found = await analysis_repo.find_by_id("job-1")
|
||||
assert found.status == AnalysisStatus.RUNNING
|
||||
assert found.started_at is not None
|
||||
|
||||
async def test_update_status_completed(self):
|
||||
await self._insert_doc()
|
||||
job = AnalysisJob(id="job-1", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
job.mark_running()
|
||||
job.mark_completed(markdown="# Test", html="<h1>Test</h1>", pages_json="[]")
|
||||
await analysis_repo.update_status(job)
|
||||
|
||||
found = await analysis_repo.find_by_id("job-1")
|
||||
assert found.status == AnalysisStatus.COMPLETED
|
||||
assert found.content_markdown == "# Test"
|
||||
assert found.content_html == "<h1>Test</h1>"
|
||||
assert found.pages_json == "[]"
|
||||
|
||||
async def test_delete(self):
|
||||
await self._insert_doc()
|
||||
job = AnalysisJob(id="job-1", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
deleted = await analysis_repo.delete("job-1")
|
||||
assert deleted is True
|
||||
|
||||
found = await analysis_repo.find_by_id("job-1")
|
||||
assert found is None
|
||||
|
||||
async def test_delete_nonexistent(self):
|
||||
deleted = await analysis_repo.delete("nonexistent")
|
||||
assert deleted is False
|
||||
|
||||
async def test_delete_by_document(self):
|
||||
await self._insert_doc()
|
||||
for i in range(3):
|
||||
job = AnalysisJob(id=f"job-{i}", document_id="doc-1")
|
||||
await analysis_repo.insert(job)
|
||||
|
||||
count = await analysis_repo.delete_by_document("doc-1")
|
||||
assert count == 3
|
||||
|
||||
all_jobs = await analysis_repo.find_all()
|
||||
assert len(all_jobs) == 0
|
||||
85
document-parser/tests/test_schemas.py
Normal file
85
document-parser/tests/test_schemas.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Tests for API schemas — camelCase serialization."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from api.schemas import (
|
||||
AnalysisResponse,
|
||||
CreateAnalysisRequest,
|
||||
DocumentResponse,
|
||||
_to_camel,
|
||||
)
|
||||
|
||||
|
||||
class TestToCamel:
|
||||
def test_single_word(self):
|
||||
assert _to_camel("status") == "status"
|
||||
|
||||
def test_two_words(self):
|
||||
assert _to_camel("document_id") == "documentId"
|
||||
|
||||
def test_three_words(self):
|
||||
assert _to_camel("content_html_raw") == "contentHtmlRaw"
|
||||
|
||||
def test_already_camel(self):
|
||||
assert _to_camel("documentId") == "documentId"
|
||||
|
||||
|
||||
class TestDocumentResponse:
|
||||
def test_serializes_to_camel_case(self):
|
||||
doc = DocumentResponse(
|
||||
id="1",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
file_size=1024,
|
||||
page_count=5,
|
||||
created_at="2024-01-01",
|
||||
)
|
||||
data = doc.model_dump(by_alias=True)
|
||||
assert "contentType" in data
|
||||
assert "fileSize" in data
|
||||
assert "pageCount" in data
|
||||
assert "createdAt" in data
|
||||
# Original snake_case should not appear
|
||||
assert "content_type" not in data
|
||||
assert "file_size" not in data
|
||||
|
||||
def test_optional_fields_default_to_none(self):
|
||||
doc = DocumentResponse(id="1", filename="test.pdf", created_at="2024-01-01")
|
||||
assert doc.content_type is None
|
||||
assert doc.file_size is None
|
||||
assert doc.page_count is None
|
||||
|
||||
|
||||
class TestAnalysisResponse:
|
||||
def test_serializes_to_camel_case(self):
|
||||
resp = AnalysisResponse(
|
||||
id="1",
|
||||
document_id="d1",
|
||||
status="COMPLETED",
|
||||
content_markdown="# Hello",
|
||||
pages_json="[]",
|
||||
created_at="2024-01-01",
|
||||
)
|
||||
data = resp.model_dump(by_alias=True)
|
||||
assert "documentId" in data
|
||||
assert "contentMarkdown" in data
|
||||
assert "pagesJson" in data
|
||||
assert "errorMessage" in data
|
||||
assert "startedAt" in data
|
||||
assert "completedAt" in data
|
||||
|
||||
def test_populate_by_name(self):
|
||||
"""Can create using snake_case field names."""
|
||||
resp = AnalysisResponse(
|
||||
id="1",
|
||||
document_id="d1",
|
||||
status="PENDING",
|
||||
created_at="2024-01-01",
|
||||
)
|
||||
assert resp.document_id == "d1"
|
||||
|
||||
|
||||
class TestCreateAnalysisRequest:
|
||||
def test_parses_document_id(self):
|
||||
req = CreateAnalysisRequest(documentId="doc-42")
|
||||
assert req.documentId == "doc-42"
|
||||
55
frontend/src/features/analysis/api.test.js
Normal file
55
frontend/src/features/analysis/api.test.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { createAnalysis, fetchAnalyses, fetchAnalysis, deleteAnalysis } from './api.js'
|
||||
|
||||
vi.mock('../../shared/api/http.js', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
import { apiFetch } from '../../shared/api/http.js'
|
||||
|
||||
describe('analysis API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('createAnalysis sends POST with documentId', async () => {
|
||||
const job = { id: '1', documentId: 'doc-1', status: 'PENDING' }
|
||||
apiFetch.mockResolvedValue(job)
|
||||
|
||||
const result = await createAnalysis('doc-1')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ documentId: 'doc-1' }),
|
||||
})
|
||||
expect(result).toEqual(job)
|
||||
})
|
||||
|
||||
it('fetchAnalyses calls GET /api/analyses', async () => {
|
||||
const jobs = [{ id: '1', status: 'COMPLETED' }]
|
||||
apiFetch.mockResolvedValue(jobs)
|
||||
|
||||
const result = await fetchAnalyses()
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses')
|
||||
expect(result).toEqual(jobs)
|
||||
})
|
||||
|
||||
it('fetchAnalysis calls GET /api/analyses/:id', async () => {
|
||||
const job = { id: '42', status: 'RUNNING' }
|
||||
apiFetch.mockResolvedValue(job)
|
||||
|
||||
const result = await fetchAnalysis('42')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses/42')
|
||||
expect(result).toEqual(job)
|
||||
})
|
||||
|
||||
it('deleteAnalysis calls DELETE /api/analyses/:id', async () => {
|
||||
apiFetch.mockResolvedValue(null)
|
||||
|
||||
await deleteAnalysis('42')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses/42', { method: 'DELETE' })
|
||||
})
|
||||
})
|
||||
155
frontend/src/features/analysis/store.test.js
Normal file
155
frontend/src/features/analysis/store.test.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useAnalysisStore } from './store.js'
|
||||
|
||||
vi.mock('./api.js', () => ({
|
||||
fetchAnalyses: vi.fn(),
|
||||
fetchAnalysis: vi.fn(),
|
||||
createAnalysis: vi.fn(),
|
||||
deleteAnalysis: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as api from './api.js'
|
||||
|
||||
describe('useAnalysisStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('starts with empty state', () => {
|
||||
const store = useAnalysisStore()
|
||||
expect(store.analyses).toEqual([])
|
||||
expect(store.currentAnalysis).toBeNull()
|
||||
expect(store.running).toBe(false)
|
||||
})
|
||||
|
||||
it('currentPages parses pagesJson from current analysis', () => {
|
||||
const store = useAnalysisStore()
|
||||
store.currentAnalysis = {
|
||||
pagesJson: JSON.stringify([{ pageNumber: 1, width: 612, height: 792 }]),
|
||||
}
|
||||
expect(store.currentPages).toEqual([{ pageNumber: 1, width: 612, height: 792 }])
|
||||
})
|
||||
|
||||
it('currentPages returns [] when no current analysis', () => {
|
||||
const store = useAnalysisStore()
|
||||
expect(store.currentPages).toEqual([])
|
||||
})
|
||||
|
||||
it('currentPages returns [] on invalid JSON', () => {
|
||||
const store = useAnalysisStore()
|
||||
store.currentAnalysis = { pagesJson: 'not json' }
|
||||
expect(store.currentPages).toEqual([])
|
||||
})
|
||||
|
||||
it('load() fetches analyses', async () => {
|
||||
const data = [{ id: '1', status: 'COMPLETED' }]
|
||||
api.fetchAnalyses.mockResolvedValue(data)
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.load()
|
||||
|
||||
expect(store.analyses).toEqual(data)
|
||||
})
|
||||
|
||||
it('run() creates analysis, sets current, and starts polling', async () => {
|
||||
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
|
||||
api.createAnalysis.mockResolvedValue(job)
|
||||
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' })
|
||||
|
||||
const store = useAnalysisStore()
|
||||
const result = await store.run('d1')
|
||||
|
||||
expect(result).toEqual(job)
|
||||
expect(store.currentAnalysis).toEqual(job)
|
||||
expect(store.analyses[0]).toEqual(job)
|
||||
expect(store.running).toBe(true)
|
||||
|
||||
// Advance timer to trigger polling
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(api.fetchAnalysis).toHaveBeenCalledWith('j1')
|
||||
expect(store.running).toBe(false) // COMPLETED stops polling
|
||||
|
||||
store.stopPolling()
|
||||
})
|
||||
|
||||
it('run() resets running on error', async () => {
|
||||
api.createAnalysis.mockRejectedValue(new Error('fail'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await expect(store.run('d1')).rejects.toThrow('fail')
|
||||
|
||||
expect(store.running).toBe(false)
|
||||
})
|
||||
|
||||
it('select() fetches and sets current analysis', async () => {
|
||||
const job = { id: '42', status: 'COMPLETED' }
|
||||
api.fetchAnalysis.mockResolvedValue(job)
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.select('42')
|
||||
|
||||
expect(store.currentAnalysis).toEqual(job)
|
||||
})
|
||||
|
||||
it('remove() deletes and removes from list', async () => {
|
||||
api.deleteAnalysis.mockResolvedValue(null)
|
||||
|
||||
const store = useAnalysisStore()
|
||||
store.analyses = [{ id: '1' }, { id: '2' }]
|
||||
store.currentAnalysis = { id: '1' }
|
||||
|
||||
await store.remove('1')
|
||||
|
||||
expect(store.analyses).toEqual([{ id: '2' }])
|
||||
expect(store.currentAnalysis).toBeNull()
|
||||
})
|
||||
|
||||
it('remove() keeps currentAnalysis if different id', async () => {
|
||||
api.deleteAnalysis.mockResolvedValue(null)
|
||||
|
||||
const store = useAnalysisStore()
|
||||
store.analyses = [{ id: '1' }, { id: '2' }]
|
||||
store.currentAnalysis = { id: '2' }
|
||||
|
||||
await store.remove('1')
|
||||
|
||||
expect(store.currentAnalysis).toEqual({ id: '2' })
|
||||
})
|
||||
|
||||
it('polling stops on FAILED status', async () => {
|
||||
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
|
||||
api.createAnalysis.mockResolvedValue(job)
|
||||
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'FAILED', errorMessage: 'oops' })
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.run('d1')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(store.running).toBe(false)
|
||||
expect(store.currentAnalysis.status).toBe('FAILED')
|
||||
})
|
||||
|
||||
it('polling stops on fetch error', async () => {
|
||||
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
|
||||
api.createAnalysis.mockResolvedValue(job)
|
||||
api.fetchAnalysis.mockRejectedValue(new Error('network'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.run('d1')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(store.running).toBe(false)
|
||||
})
|
||||
})
|
||||
65
frontend/src/features/document/api.test.js
Normal file
65
frontend/src/features/document/api.test.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { fetchDocuments, fetchDocument, uploadDocument, deleteDocument, getPreviewUrl } from './api.js'
|
||||
|
||||
vi.mock('../../shared/api/http.js', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
import { apiFetch } from '../../shared/api/http.js'
|
||||
|
||||
describe('document API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetchDocuments calls GET /api/documents', async () => {
|
||||
const docs = [{ id: '1', filename: 'a.pdf' }]
|
||||
apiFetch.mockResolvedValue(docs)
|
||||
|
||||
const result = await fetchDocuments()
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/documents')
|
||||
expect(result).toEqual(docs)
|
||||
})
|
||||
|
||||
it('fetchDocument calls GET /api/documents/:id', async () => {
|
||||
const doc = { id: '42', filename: 'test.pdf' }
|
||||
apiFetch.mockResolvedValue(doc)
|
||||
|
||||
const result = await fetchDocument('42')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/documents/42')
|
||||
expect(result).toEqual(doc)
|
||||
})
|
||||
|
||||
it('uploadDocument sends file via FormData with skipContentType', async () => {
|
||||
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||
const response = { id: '1', filename: 'test.pdf' }
|
||||
apiFetch.mockResolvedValue(response)
|
||||
|
||||
const result = await uploadDocument(file)
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/documents/upload', {
|
||||
method: 'POST',
|
||||
body: expect.any(FormData),
|
||||
skipContentType: true,
|
||||
})
|
||||
expect(result).toEqual(response)
|
||||
})
|
||||
|
||||
it('deleteDocument calls DELETE /api/documents/:id', async () => {
|
||||
apiFetch.mockResolvedValue(null)
|
||||
|
||||
await deleteDocument('42')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/documents/42', { method: 'DELETE' })
|
||||
})
|
||||
|
||||
it('getPreviewUrl builds correct URL with defaults', () => {
|
||||
expect(getPreviewUrl('abc')).toBe('/api/documents/abc/preview?page=1&dpi=150')
|
||||
})
|
||||
|
||||
it('getPreviewUrl accepts custom page and dpi', () => {
|
||||
expect(getPreviewUrl('abc', 3, 300)).toBe('/api/documents/abc/preview?page=3&dpi=300')
|
||||
})
|
||||
})
|
||||
115
frontend/src/features/document/store.test.js
Normal file
115
frontend/src/features/document/store.test.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useDocumentStore } from './store.js'
|
||||
|
||||
vi.mock('./api.js', () => ({
|
||||
fetchDocuments: vi.fn(),
|
||||
uploadDocument: vi.fn(),
|
||||
deleteDocument: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as api from './api.js'
|
||||
|
||||
describe('useDocumentStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with empty state', () => {
|
||||
const store = useDocumentStore()
|
||||
expect(store.documents).toEqual([])
|
||||
expect(store.selectedId).toBeNull()
|
||||
expect(store.uploading).toBe(false)
|
||||
})
|
||||
|
||||
it('load() fetches and sets documents', async () => {
|
||||
const docs = [{ id: '1', filename: 'a.pdf' }, { id: '2', filename: 'b.pdf' }]
|
||||
api.fetchDocuments.mockResolvedValue(docs)
|
||||
|
||||
const store = useDocumentStore()
|
||||
await store.load()
|
||||
|
||||
expect(store.documents).toEqual(docs)
|
||||
})
|
||||
|
||||
it('load() handles errors gracefully', async () => {
|
||||
api.fetchDocuments.mockRejectedValue(new Error('network'))
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useDocumentStore()
|
||||
await store.load()
|
||||
|
||||
expect(store.documents).toEqual([])
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
it('upload() adds document to front of list and selects it', async () => {
|
||||
const newDoc = { id: 'new', filename: 'new.pdf' }
|
||||
api.uploadDocument.mockResolvedValue(newDoc)
|
||||
|
||||
const store = useDocumentStore()
|
||||
store.documents = [{ id: 'old', filename: 'old.pdf' }]
|
||||
|
||||
const result = await store.upload(new File([], 'new.pdf'))
|
||||
|
||||
expect(result).toEqual(newDoc)
|
||||
expect(store.documents[0]).toEqual(newDoc)
|
||||
expect(store.selectedId).toBe('new')
|
||||
expect(store.uploading).toBe(false)
|
||||
})
|
||||
|
||||
it('upload() sets uploading to true during upload', async () => {
|
||||
let resolveUpload
|
||||
api.uploadDocument.mockImplementation(() => new Promise(r => { resolveUpload = r }))
|
||||
|
||||
const store = useDocumentStore()
|
||||
const promise = store.upload(new File([], 'test.pdf'))
|
||||
|
||||
expect(store.uploading).toBe(true)
|
||||
resolveUpload({ id: '1', filename: 'test.pdf' })
|
||||
await promise
|
||||
expect(store.uploading).toBe(false)
|
||||
})
|
||||
|
||||
it('upload() resets uploading on error', async () => {
|
||||
api.uploadDocument.mockRejectedValue(new Error('fail'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useDocumentStore()
|
||||
|
||||
await expect(store.upload(new File([], 'test.pdf'))).rejects.toThrow('fail')
|
||||
expect(store.uploading).toBe(false)
|
||||
})
|
||||
|
||||
it('remove() deletes document and clears selection if needed', async () => {
|
||||
api.deleteDocument.mockResolvedValue(null)
|
||||
|
||||
const store = useDocumentStore()
|
||||
store.documents = [{ id: '1' }, { id: '2' }]
|
||||
store.selectedId = '1'
|
||||
|
||||
await store.remove('1')
|
||||
|
||||
expect(store.documents).toEqual([{ id: '2' }])
|
||||
expect(store.selectedId).toBeNull()
|
||||
})
|
||||
|
||||
it('remove() does not clear selection for other documents', async () => {
|
||||
api.deleteDocument.mockResolvedValue(null)
|
||||
|
||||
const store = useDocumentStore()
|
||||
store.documents = [{ id: '1' }, { id: '2' }]
|
||||
store.selectedId = '2'
|
||||
|
||||
await store.remove('1')
|
||||
|
||||
expect(store.selectedId).toBe('2')
|
||||
})
|
||||
|
||||
it('select() sets selectedId', () => {
|
||||
const store = useDocumentStore()
|
||||
store.select('42')
|
||||
expect(store.selectedId).toBe('42')
|
||||
})
|
||||
})
|
||||
32
frontend/src/features/history/api.test.js
Normal file
32
frontend/src/features/history/api.test.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { fetchAnalyses, deleteAnalysis } from './api.js'
|
||||
|
||||
vi.mock('../../shared/api/http.js', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
import { apiFetch } from '../../shared/api/http.js'
|
||||
|
||||
describe('history API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetchAnalyses calls GET /api/analyses', async () => {
|
||||
const data = [{ id: '1' }, { id: '2' }]
|
||||
apiFetch.mockResolvedValue(data)
|
||||
|
||||
const result = await fetchAnalyses()
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses')
|
||||
expect(result).toEqual(data)
|
||||
})
|
||||
|
||||
it('deleteAnalysis calls DELETE /api/analyses/:id', async () => {
|
||||
apiFetch.mockResolvedValue(null)
|
||||
|
||||
await deleteAnalysis('5')
|
||||
|
||||
expect(apiFetch).toHaveBeenCalledWith('/api/analyses/5', { method: 'DELETE' })
|
||||
})
|
||||
})
|
||||
66
frontend/src/features/history/store.test.js
Normal file
66
frontend/src/features/history/store.test.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useHistoryStore } from './store.js'
|
||||
|
||||
vi.mock('./api.js', () => ({
|
||||
fetchAnalyses: vi.fn(),
|
||||
deleteAnalysis: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as api from './api.js'
|
||||
|
||||
describe('useHistoryStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with empty analyses', () => {
|
||||
const store = useHistoryStore()
|
||||
expect(store.analyses).toEqual([])
|
||||
})
|
||||
|
||||
it('load() fetches analyses', async () => {
|
||||
const data = [{ id: '1' }, { id: '2' }]
|
||||
api.fetchAnalyses.mockResolvedValue(data)
|
||||
|
||||
const store = useHistoryStore()
|
||||
await store.load()
|
||||
|
||||
expect(store.analyses).toEqual(data)
|
||||
})
|
||||
|
||||
it('load() handles errors gracefully', async () => {
|
||||
api.fetchAnalyses.mockRejectedValue(new Error('fail'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useHistoryStore()
|
||||
await store.load()
|
||||
|
||||
expect(store.analyses).toEqual([])
|
||||
})
|
||||
|
||||
it('remove() deletes and removes from list', async () => {
|
||||
api.deleteAnalysis.mockResolvedValue(null)
|
||||
|
||||
const store = useHistoryStore()
|
||||
store.analyses = [{ id: '1' }, { id: '2' }, { id: '3' }]
|
||||
|
||||
await store.remove('2')
|
||||
|
||||
expect(store.analyses).toEqual([{ id: '1' }, { id: '3' }])
|
||||
})
|
||||
|
||||
it('remove() handles errors gracefully', async () => {
|
||||
api.deleteAnalysis.mockRejectedValue(new Error('fail'))
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useHistoryStore()
|
||||
store.analyses = [{ id: '1' }]
|
||||
|
||||
await store.remove('1')
|
||||
|
||||
// Should not remove on error
|
||||
expect(store.analyses).toEqual([{ id: '1' }])
|
||||
})
|
||||
})
|
||||
95
frontend/src/shared/api/http.test.js
Normal file
95
frontend/src/shared/api/http.test.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { apiFetch } from './http.js'
|
||||
|
||||
describe('apiFetch', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('sets Content-Type to application/json by default', async () => {
|
||||
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ data: 1 }),
|
||||
})
|
||||
|
||||
await apiFetch('/api/test')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('/api/test', expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
||||
}))
|
||||
})
|
||||
|
||||
it('skips Content-Type when skipContentType is true', async () => {
|
||||
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
|
||||
await apiFetch('/api/upload', { skipContentType: true })
|
||||
|
||||
const headers = spy.mock.calls[0][1].headers
|
||||
expect(headers['Content-Type']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns parsed JSON on success', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ id: '123', name: 'doc.pdf' }),
|
||||
})
|
||||
|
||||
const result = await apiFetch('/api/docs')
|
||||
expect(result).toEqual({ id: '123', name: 'doc.pdf' })
|
||||
})
|
||||
|
||||
it('returns null on 204 No Content', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 204,
|
||||
})
|
||||
|
||||
const result = await apiFetch('/api/docs/1', { method: 'DELETE' })
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
await expect(apiFetch('/api/docs/missing')).rejects.toThrow('API error: 404')
|
||||
})
|
||||
|
||||
it('forwards method and body options', async () => {
|
||||
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
|
||||
const body = JSON.stringify({ documentId: '42' })
|
||||
await apiFetch('/api/analyses', { method: 'POST', body })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('/api/analyses', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body,
|
||||
}))
|
||||
})
|
||||
|
||||
it('merges custom headers with Content-Type', async () => {
|
||||
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({}),
|
||||
})
|
||||
|
||||
await apiFetch('/api/test', { headers: { 'X-Custom': 'value' } })
|
||||
|
||||
const headers = spy.mock.calls[0][1].headers
|
||||
expect(headers['Content-Type']).toBe('application/json')
|
||||
expect(headers['X-Custom']).toBe('value')
|
||||
})
|
||||
})
|
||||
62
frontend/src/shared/i18n.test.js
Normal file
62
frontend/src/shared/i18n.test.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
// Mock settings store before importing i18n
|
||||
vi.mock('../features/settings/store.js', () => ({
|
||||
useSettingsStore: vi.fn(),
|
||||
}))
|
||||
|
||||
import { useSettingsStore } from '../features/settings/store.js'
|
||||
import { useI18n } from './i18n.js'
|
||||
|
||||
describe('useI18n', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns French translation by default', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'fr' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('nav.studio')).toBe('Studio')
|
||||
expect(t('nav.history')).toBe('Historique')
|
||||
expect(t('nav.settings')).toBe('Paramètres')
|
||||
})
|
||||
|
||||
it('returns English translation when locale is en', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'en' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('nav.history')).toBe('History')
|
||||
expect(t('nav.settings')).toBe('Settings')
|
||||
})
|
||||
|
||||
it('falls back to French when key missing in current locale', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'de' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('nav.studio')).toBe('Studio')
|
||||
})
|
||||
|
||||
it('returns key when not found in any locale', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'fr' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('unknown.key')).toBe('unknown.key')
|
||||
})
|
||||
|
||||
it('interpolates parameters', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'en' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('results.pageOf', { current: 3, total: 10 })).toBe('Page 3 of 10')
|
||||
})
|
||||
|
||||
it('interpolates parameters in French', () => {
|
||||
useSettingsStore.mockReturnValue({ locale: 'fr' })
|
||||
|
||||
const { t } = useI18n()
|
||||
expect(t('results.pageOf', { current: 1, total: 5 })).toBe('Page 1 sur 5')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue