feat: v3.0 - canvas editor, JSON-only, no Excel, new UI
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
- Remove all Excel code (import, export, template, pandas, openpyxl) - New canvas-based schedule editor with drag & drop (interact.js) - Modern 3-panel UI: sidebar, canvas, documentation tab - New data model: Block with id/date/start/end, ProgramType with id/name/color - Clean API: GET /api/health, POST /api/validate, GET /api/sample, POST /api/generate-pdf - Rewritten PDF generator using ScenarioDocument directly (no DataFrame) - Professional PDF output: dark header, colored blocks, merged cells, legend, footer - Sample JSON: "Zimní výjezd oddílu" with 11 blocks, 3 program types - 30 tests passing (API, core models, PDF generation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
"""
|
||||
API endpoint tests using FastAPI TestClient.
|
||||
API endpoint tests for Scenar Creator v3.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -16,11 +13,20 @@ def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def make_excel_bytes(df: pd.DataFrame) -> bytes:
|
||||
bio = io.BytesIO()
|
||||
with pd.ExcelWriter(bio, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False)
|
||||
return bio.getvalue()
|
||||
def make_valid_doc():
|
||||
return {
|
||||
"version": "1.0",
|
||||
"event": {"title": "Test Event"},
|
||||
"program_types": [{"id": "ws", "name": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": [{
|
||||
"id": "b1",
|
||||
"date": "2026-03-01",
|
||||
"start": "09:00",
|
||||
"end": "10:00",
|
||||
"title": "Opening",
|
||||
"type_id": "ws"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
def test_health(client):
|
||||
@@ -28,28 +34,18 @@ def test_health(client):
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["version"] == "2.0.0"
|
||||
assert data["version"] == "3.0.0"
|
||||
|
||||
|
||||
def test_root_returns_html(client):
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert "text/html" in r.headers["content-type"]
|
||||
assert "Scenar Creator" in r.text
|
||||
assert "Scen" in r.text and "Creator" in r.text
|
||||
|
||||
|
||||
def test_validate_valid(client):
|
||||
doc = {
|
||||
"event": {"title": "Test", "detail": "Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": [{
|
||||
"datum": "2025-11-13",
|
||||
"zacatek": "09:00:00",
|
||||
"konec": "10:00:00",
|
||||
"program": "Opening",
|
||||
"typ": "WS"
|
||||
}]
|
||||
}
|
||||
doc = make_valid_doc()
|
||||
r = client.post("/api/validate", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
@@ -58,17 +54,8 @@ def test_validate_valid(client):
|
||||
|
||||
|
||||
def test_validate_unknown_type(client):
|
||||
doc = {
|
||||
"event": {"title": "Test", "detail": "Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": [{
|
||||
"datum": "2025-11-13",
|
||||
"zacatek": "09:00:00",
|
||||
"konec": "10:00:00",
|
||||
"program": "Opening",
|
||||
"typ": "UNKNOWN"
|
||||
}]
|
||||
}
|
||||
doc = make_valid_doc()
|
||||
doc["blocks"][0]["type_id"] = "UNKNOWN"
|
||||
r = client.post("/api/validate", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
@@ -77,102 +64,81 @@ def test_validate_unknown_type(client):
|
||||
|
||||
|
||||
def test_validate_bad_time_order(client):
|
||||
doc = {
|
||||
"event": {"title": "Test", "detail": "Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": [{
|
||||
"datum": "2025-11-13",
|
||||
"zacatek": "10:00:00",
|
||||
"konec": "09:00:00",
|
||||
"program": "Bad",
|
||||
"typ": "WS"
|
||||
}]
|
||||
}
|
||||
doc = make_valid_doc()
|
||||
doc["blocks"][0]["start"] = "10:00"
|
||||
doc["blocks"][0]["end"] = "09:00"
|
||||
r = client.post("/api/validate", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["valid"] is False
|
||||
assert any("start time" in e for e in data["errors"])
|
||||
|
||||
|
||||
def test_validate_no_blocks(client):
|
||||
doc = make_valid_doc()
|
||||
doc["blocks"] = []
|
||||
r = client.post("/api/validate", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["valid"] is False
|
||||
|
||||
|
||||
def test_import_excel(client):
|
||||
df = pd.DataFrame({
|
||||
'Datum': [pd.Timestamp('2025-11-13').date()],
|
||||
'Zacatek': ['09:00'],
|
||||
'Konec': ['10:00'],
|
||||
'Program': ['Test Program'],
|
||||
'Typ': ['WORKSHOP'],
|
||||
'Garant': ['John'],
|
||||
'Poznamka': ['Note']
|
||||
})
|
||||
|
||||
content = make_excel_bytes(df)
|
||||
r = client.post(
|
||||
"/api/import-excel",
|
||||
files={"file": ("test.xlsx", content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
|
||||
data={"title": "Imported Event", "detail": "From Excel"}
|
||||
)
|
||||
|
||||
def test_validate_no_types(client):
|
||||
doc = make_valid_doc()
|
||||
doc["program_types"] = []
|
||||
r = client.post("/api/validate", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["success"] is True
|
||||
assert data["document"] is not None
|
||||
assert data["document"]["event"]["title"] == "Imported Event"
|
||||
assert len(data["document"]["blocks"]) == 1
|
||||
assert data["valid"] is False
|
||||
|
||||
|
||||
def test_generate_excel(client):
|
||||
doc = {
|
||||
"event": {"title": "Test Event", "detail": "Test Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#0070C0"}],
|
||||
"blocks": [{
|
||||
"datum": "2025-11-13",
|
||||
"zacatek": "09:00:00",
|
||||
"konec": "10:00:00",
|
||||
"program": "Opening",
|
||||
"typ": "WS",
|
||||
"garant": "John",
|
||||
"poznamka": "Note"
|
||||
}]
|
||||
}
|
||||
r = client.post("/api/generate-excel", json=doc)
|
||||
def test_sample_endpoint(client):
|
||||
r = client.get("/api/sample")
|
||||
assert r.status_code == 200
|
||||
assert "spreadsheetml" in r.headers["content-type"]
|
||||
assert len(r.content) > 0
|
||||
data = r.json()
|
||||
assert data["version"] == "1.0"
|
||||
assert data["event"]["title"] == "Zimní výjezd oddílu"
|
||||
assert len(data["program_types"]) == 3
|
||||
assert len(data["blocks"]) >= 8
|
||||
|
||||
|
||||
def test_generate_excel_no_blocks(client):
|
||||
doc = {
|
||||
"event": {"title": "Test", "detail": "Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": []
|
||||
}
|
||||
r = client.post("/api/generate-excel", json=doc)
|
||||
def test_sample_blocks_valid(client):
|
||||
r = client.get("/api/sample")
|
||||
data = r.json()
|
||||
type_ids = {pt["id"] for pt in data["program_types"]}
|
||||
for block in data["blocks"]:
|
||||
assert block["type_id"] in type_ids
|
||||
assert block["start"] < block["end"]
|
||||
|
||||
|
||||
def test_generate_pdf(client):
|
||||
doc = make_valid_doc()
|
||||
r = client.post("/api/generate-pdf", json=doc)
|
||||
assert r.status_code == 200
|
||||
assert r.headers["content-type"] == "application/pdf"
|
||||
assert r.content[:5] == b'%PDF-'
|
||||
|
||||
|
||||
def test_generate_pdf_no_blocks(client):
|
||||
doc = make_valid_doc()
|
||||
doc["blocks"] = []
|
||||
r = client.post("/api/generate-pdf", json=doc)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_export_json(client):
|
||||
doc = {
|
||||
"event": {"title": "Test", "detail": "Detail"},
|
||||
"program_types": [{"code": "WS", "description": "Workshop", "color": "#FF0000"}],
|
||||
"blocks": [{
|
||||
"datum": "2025-11-13",
|
||||
"zacatek": "09:00:00",
|
||||
"konec": "10:00:00",
|
||||
"program": "Opening",
|
||||
"typ": "WS"
|
||||
}]
|
||||
}
|
||||
r = client.post("/api/export-json", json=doc)
|
||||
def test_generate_pdf_multiday(client):
|
||||
doc = make_valid_doc()
|
||||
doc["blocks"].append({
|
||||
"id": "b2",
|
||||
"date": "2026-03-02",
|
||||
"start": "14:00",
|
||||
"end": "15:00",
|
||||
"title": "Day 2 Session",
|
||||
"type_id": "ws"
|
||||
})
|
||||
r = client.post("/api/generate-pdf", json=doc)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["event"]["title"] == "Test"
|
||||
assert len(data["blocks"]) == 1
|
||||
|
||||
|
||||
def test_template_download(client):
|
||||
r = client.get("/api/template")
|
||||
# Template might not exist in test env, but endpoint should work
|
||||
assert r.status_code in [200, 404]
|
||||
assert r.content[:5] == b'%PDF-'
|
||||
|
||||
|
||||
def test_swagger_docs(client):
|
||||
|
||||
Reference in New Issue
Block a user