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.
72 lines
2.8 KiB
Python
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
|