POSA_Copyrighter/tests/operator_gui/test_decision_flow.py
changukyu 0bfa30d7f5 test: add Playwright E2E harness for operator GUI with decision-flow spec
Introduce a reusable browser-test harness under tests/operator_gui/:
- conftest.py: shared fixtures (operator_server, browser with graceful skip,
  operator_page with screenshot-on-failure artifacts)
- pages/: OperatorWorkbench Page Object encapsulating selectors and actions
- refactor test_browser_smoke.py onto the fixtures + POM (drops duplicated
  server/browser plumbing)
- test_decision_flow.py: approve/hold/reject E2E against the real server,
  covering the reject-requires-memo guard and decision persistence

Pin playwright in requirements-dev.txt (Chromium bundled offline for the
air-gapped target) and ignore the artifacts/ screenshot dir.
2026-06-21 21:10:22 +09:00

72 lines
2.8 KiB
Python

"""Operator decision flow (approve / hold / reject) end-to-end.
Runs against the real server (no bootstrap stub): each test uploads an image to
seed a genuine submission, then exercises the decision pane and asserts the
decision both renders and persists through ``refreshFromApi``.
"""
from __future__ import annotations
from pathlib import Path
from .pages import OperatorWorkbench
from .pages.workbench_page import MEMO_REQUIRED_ERROR
_SVG = "<svg xmlns='http://www.w3.org/2000/svg' width='80' height='60'></svg>"
def _open_seeded_case(workbench: OperatorWorkbench, tmp_path: Path, case_id: str = "decide-me") -> str:
upload_file = tmp_path / f"{case_id}.svg"
upload_file.write_text(_SVG, encoding="utf-8")
workbench.goto().upload_and_open(str(upload_file), case_id)
# A freshly uploaded submission starts unreviewed.
assert "현재 운영 결정: 미심사" in workbench.recommendation_box.inner_text()
return case_id
def test_reject_without_memo_is_blocked_and_not_persisted(operator_server, operator_page, tmp_path):
workbench = OperatorWorkbench(operator_page, operator_server.base_url)
_open_seeded_case(workbench, tmp_path)
workbench.decide("rejected") # 반려 requires a memo
assert workbench.memo_error.inner_text() == MEMO_REQUIRED_ERROR
# The guard fires client-side, so the decision is never sent or persisted.
assert "현재 운영 결정: 미심사" in workbench.recommendation_box.inner_text()
def test_approve_persists_without_memo(operator_server, operator_page, tmp_path):
workbench = OperatorWorkbench(operator_page, operator_server.base_url)
_open_seeded_case(workbench, tmp_path)
workbench.decide("approved") # 승인 needs no memo
workbench.wait_for_decision_label("승인")
assert workbench.memo_error.inner_text() == ""
assert not workbench.console_errors
def test_hold_persists_without_memo(operator_server, operator_page, tmp_path):
workbench = OperatorWorkbench(operator_page, operator_server.base_url)
_open_seeded_case(workbench, tmp_path)
workbench.decide("held") # 보류 needs no memo
workbench.wait_for_decision_label("보류")
assert workbench.memo_error.inner_text() == ""
assert not workbench.console_errors
def test_reject_with_memo_persists_and_clears_memo(operator_server, operator_page, tmp_path):
workbench = OperatorWorkbench(operator_page, operator_server.base_url)
_open_seeded_case(workbench, tmp_path)
workbench.set_decision_memo("저작권 침해 소지")
workbench.decide("rejected")
workbench.wait_for_decision_label("반려")
assert workbench.memo_error.inner_text() == ""
# The memo field is cleared after a successful decision.
assert workbench.decision_memo.input_value() == ""
assert not workbench.console_errors