운영에서 사용자 디자인이 1122x1402로 작성됨. 1080x1350과 정확히 같은 4:5 종횡비지만 절대 사이즈만 다르므로 정확한 사이즈 강제는 과도. - 검증: 종횡비 4:5 (±2% tolerance). 1080x1350·1122x1402 등 동일 비율 높은 해상도 모두 통과. - Vision은 base64로 원본 분석 (사이즈 무관). - Playwright는 background-size: cover로 1080x1350 컨테이너에 자동 fit. - 비율이 깨지면 (예: 1024x1024 정사각) 여전히 reject. test_validate_images_accepts_higher_resolution_4_5_ratio 신규 (1 case).
177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
"""design_importer 회귀 테스트."""
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app import design_importer
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_theme(tmp_path):
|
|
"""templates/<theme>/pages/ 구조를 가진 임시 디렉토리."""
|
|
pages = tmp_path / "minimal" / "pages"
|
|
pages.mkdir(parents=True)
|
|
return tmp_path / "minimal"
|
|
|
|
|
|
def _touch(pages_dir: Path, names: list[str]):
|
|
for n in names:
|
|
(pages_dir / n).write_bytes(b"") # 매핑 테스트는 dimension 검증 안 함
|
|
|
|
|
|
def test_auto_page_mapping_with_cover_and_cta(tmp_theme):
|
|
"""cover 키워드 → 1, cta 키워드 → 10, 나머지는 알파벳 순 2~9."""
|
|
_touch(tmp_theme / "pages", [
|
|
"insta_card_start.png", # start → page 1 (cover priority)
|
|
"insta_card_keyword.png",
|
|
"insta_card_highlight.png",
|
|
"insta_card_observation.png",
|
|
"insta_card_memo.png",
|
|
"insta_card_oneline.png",
|
|
"insta_card_checklist.png",
|
|
"insta_card_study.png",
|
|
"insta_card_cta.png", # cta → page 10
|
|
"insta_card_finish.png", # finish은 cta가 이미 채워 본문 풀로
|
|
])
|
|
mapping = design_importer._resolve_page_mapping(tmp_theme / "pages")
|
|
assert mapping["insta_card_start.png"] == 1
|
|
assert mapping["insta_card_cta.png"] == 10
|
|
# 본문 풀 (남은 8장)은 알파벳 정렬: checklist, finish, highlight, keyword, memo, observation, oneline, study
|
|
body_pages = {p: n for n, p in mapping.items() if 2 <= p <= 9}
|
|
assert body_pages[2] == "insta_card_checklist.png"
|
|
assert body_pages[3] == "insta_card_finish.png"
|
|
assert body_pages[9] == "insta_card_study.png"
|
|
assert set(mapping.values()) == set(range(1, 11))
|
|
|
|
|
|
def test_explicit_order_json_overrides_auto_mapping(tmp_theme):
|
|
"""_order.json이 있으면 자동 매핑보다 우선."""
|
|
pages = tmp_theme / "pages"
|
|
_touch(pages, [
|
|
"insta_card_start.png",
|
|
"insta_card_cta.png",
|
|
"insta_card_finish.png",
|
|
] + [f"insta_card_body{i}.png" for i in range(1, 8)])
|
|
(pages / "_order.json").write_text(json.dumps({
|
|
"insta_card_start.png": 1,
|
|
"insta_card_finish.png": 10, # cta 대신 finish를 page 10으로
|
|
"insta_card_cta.png": 5, # cta를 본문 한가운데로 강제
|
|
"insta_card_body1.png": 2,
|
|
"insta_card_body2.png": 3,
|
|
"insta_card_body3.png": 4,
|
|
"insta_card_body4.png": 6,
|
|
"insta_card_body5.png": 7,
|
|
"insta_card_body6.png": 8,
|
|
"insta_card_body7.png": 9,
|
|
}), encoding="utf-8")
|
|
mapping = design_importer._resolve_page_mapping(pages)
|
|
assert mapping["insta_card_finish.png"] == 10
|
|
assert mapping["insta_card_cta.png"] == 5
|
|
assert mapping["insta_card_start.png"] == 1
|
|
|
|
|
|
def test_validates_exactly_ten_pngs(tmp_theme):
|
|
"""PNG가 정확히 10장이 아니면 ValueError."""
|
|
_touch(tmp_theme / "pages", [f"x{i}.png" for i in range(5)]) # 5장
|
|
with pytest.raises(ValueError, match="10"):
|
|
design_importer._resolve_page_mapping(tmp_theme / "pages")
|
|
|
|
|
|
def _make_png(path: Path, size: tuple[int, int]) -> None:
|
|
"""size 픽셀의 단색 PNG를 생성."""
|
|
from PIL import Image
|
|
Image.new("RGB", size, color=(200, 200, 200)).save(path, format="PNG")
|
|
|
|
|
|
def test_validate_images_accepts_higher_resolution_4_5_ratio(tmp_theme):
|
|
"""1080x1350 외에도 같은 4:5 비율이면 통과 (예: 1122x1402, 디자인 도구 export 흔한 사이즈)."""
|
|
pages = tmp_theme / "pages"
|
|
for i in range(10):
|
|
_make_png(pages / f"insta_card_{i:02d}.png", (1122, 1402))
|
|
design_importer._validate_images(pages) # 예외 없으면 통과
|
|
|
|
|
|
def test_validate_images_accepts_1080x1350(tmp_theme):
|
|
pages = tmp_theme / "pages"
|
|
for i in range(10):
|
|
_make_png(pages / f"insta_card_{i:02d}.png", (1080, 1350))
|
|
# 예외 없이 통과해야 함
|
|
design_importer._validate_images(pages)
|
|
|
|
|
|
def test_validate_images_rejects_wrong_dimensions(tmp_theme):
|
|
pages = tmp_theme / "pages"
|
|
for i in range(10):
|
|
size = (800, 800) if i == 5 else (1080, 1350)
|
|
_make_png(pages / f"insta_card_{i:02d}.png", size)
|
|
with pytest.raises(ValueError, match="1080x1350"):
|
|
design_importer._validate_images(pages)
|
|
|
|
|
|
def test_import_design_theme_writes_html_via_mocked_vision(tmp_theme, monkeypatch):
|
|
"""Vision mock이 정상 HTML 반환 시 card.html.j2 파일이 저장되고 결과 dict 반환."""
|
|
pages = tmp_theme / "pages"
|
|
names = [
|
|
"insta_card_start.png",
|
|
"insta_card_cta.png",
|
|
] + [f"insta_card_body{i}.png" for i in range(8)]
|
|
for n in names:
|
|
_make_png(pages / n, (1080, 1350))
|
|
|
|
fake_html = """<!DOCTYPE html><html><body>
|
|
{% if page_no == 1 %}<div class="cover">{{ headline }}</div>{% endif %}
|
|
{% if page_no >= 2 and page_no <= 9 %}<div class="body">{{ headline }}<p>{{ body }}</p></div>{% endif %}
|
|
{% if page_no == 10 %}<div class="cta">{{ headline }}<p>{{ cta }}</p></div>{% endif %}
|
|
</body></html>"""
|
|
|
|
def fake_vision_call(images_with_pages, theme_name):
|
|
return {"html": fake_html, "tokens": 12345, "summary": "test summary"}
|
|
|
|
monkeypatch.setattr(design_importer, "_call_vision", fake_vision_call)
|
|
|
|
result = design_importer.import_design_theme(str(tmp_theme))
|
|
assert result["theme_name"] == "minimal"
|
|
assert "card.html.j2" in result["html_path"]
|
|
assert (tmp_theme / "card.html.j2").exists()
|
|
assert (tmp_theme / "card.html.j2").read_text(encoding="utf-8") == fake_html
|
|
assert "insta_card_start.png" in result["page_mapping"]
|
|
assert result["tokens_used"] == 12345
|
|
|
|
|
|
def test_import_design_theme_raises_on_jinja_parse_failure(tmp_theme, monkeypatch):
|
|
"""Vision이 깨진 Jinja 반환 시 ValueError + .error.txt 보존."""
|
|
pages = tmp_theme / "pages"
|
|
for i in range(10):
|
|
_make_png(pages / f"insta_card_{i:02d}.png", (1080, 1350))
|
|
|
|
broken_html = "<div>{% if page_no == 1 unclosed"
|
|
|
|
monkeypatch.setattr(design_importer, "_call_vision",
|
|
lambda imgs, name: {"html": broken_html, "tokens": 100, "summary": ""})
|
|
|
|
with pytest.raises(ValueError, match="Jinja"):
|
|
design_importer.import_design_theme(str(tmp_theme))
|
|
assert (tmp_theme / "card.html.j2.error.txt").exists()
|
|
|
|
|
|
def test_import_design_theme_backs_up_existing_html(tmp_theme, monkeypatch):
|
|
"""기존 card.html.j2가 있으면 .bak.YYYYMMDD-HHMMSS로 백업 후 새로 작성."""
|
|
pages = tmp_theme / "pages"
|
|
for i in range(10):
|
|
_make_png(pages / f"insta_card_{i:02d}.png", (1080, 1350))
|
|
(tmp_theme / "card.html.j2").write_text("OLD HTML", encoding="utf-8")
|
|
|
|
monkeypatch.setattr(design_importer, "_call_vision",
|
|
lambda imgs, name: {"html": "<div>{{ headline }}</div>", "tokens": 50, "summary": ""})
|
|
|
|
design_importer.import_design_theme(str(tmp_theme))
|
|
# .bak.* 파일이 생성되었어야 함
|
|
backups = list(tmp_theme.glob("card.html.j2.bak.*"))
|
|
assert len(backups) == 1
|
|
assert backups[0].read_text(encoding="utf-8") == "OLD HTML"
|
|
# 새 파일은 새 내용
|
|
assert "headline" in (tmp_theme / "card.html.j2").read_text(encoding="utf-8")
|