Skip to content

Commit e71f887

Browse files
committed
test(preview_questions): add unit tests for preview question builder
1 parent efe52fa commit e71f887

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

tests/test_preview_questions.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from commitizen.cz.base import BaseCommitizen
6+
from commitizen.preview_questions import build_preview_questions
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Mapping
10+
11+
import pytest
12+
13+
from commitizen.question import CzQuestion
14+
15+
16+
class PreviewCz(BaseCommitizen):
17+
def __init__(self, config) -> None:
18+
super().__init__(config)
19+
self.calls: list[dict[str, Any]] = []
20+
21+
def questions(self) -> list[CzQuestion]:
22+
return []
23+
24+
def message(self, answers: Mapping[str, Any]) -> str:
25+
self.calls.append(dict(answers))
26+
return f"{answers.get('prefix', '')}: {answers.get('subject', '')}".strip()
27+
28+
def schema(self) -> str:
29+
return ""
30+
31+
def schema_pattern(self) -> str:
32+
return ""
33+
34+
def example(self) -> str:
35+
return ""
36+
37+
def info(self) -> str:
38+
return ""
39+
40+
41+
def test_build_preview_questions_disabled_returns_original_list(config):
42+
cz = PreviewCz(config)
43+
questions: list[CzQuestion] = [
44+
{"type": "input", "name": "subject", "message": "Subject"},
45+
]
46+
47+
out = build_preview_questions(cz, questions, enabled=False, max_length=50)
48+
assert out is questions
49+
50+
51+
def test_build_preview_questions_wraps_filter_and_updates_answers_state(
52+
monkeypatch: pytest.MonkeyPatch,
53+
config,
54+
):
55+
cz = PreviewCz(config)
56+
57+
def original_filter(raw: str) -> str:
58+
return raw.strip().upper()
59+
60+
class DummyBuffer:
61+
text = " hello "
62+
63+
class DummyLayout:
64+
current_buffer = DummyBuffer()
65+
66+
class DummyApp:
67+
layout = DummyLayout()
68+
69+
questions: list[CzQuestion] = [
70+
{
71+
"type": "input",
72+
"name": "subject",
73+
"message": "Subject",
74+
"filter": original_filter,
75+
}
76+
]
77+
78+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
79+
q = enhanced[0]
80+
assert q["filter"] is not original_filter
81+
82+
# First update state via filter wrapper
83+
assert q["filter"](" hello ") == "HELLO"
84+
85+
# Then call toolbar which uses subject_builder -> cz.message() using answers_state
86+
# and the current buffer text for the active field.
87+
monkeypatch.setattr("commitizen.preview_questions.get_app", lambda: DummyApp())
88+
toolbar_text = q["bottom_toolbar"]()
89+
assert "HELLO" in toolbar_text
90+
assert cz.calls, "cz.message should be called by toolbar rendering"
91+
assert cz.calls[-1]["subject"] == "HELLO"
92+
93+
94+
def test_build_preview_questions_adds_toolbar_only_for_supported_types(config):
95+
cz = PreviewCz(config)
96+
questions: list[CzQuestion] = [
97+
{"type": "input", "name": "a", "message": "A"},
98+
{"type": "confirm", "name": "b", "message": "B"},
99+
{"type": "list", "name": "c", "message": "C", "choices": []},
100+
]
101+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
102+
103+
assert callable(enhanced[0].get("bottom_toolbar"))
104+
assert callable(enhanced[1].get("bottom_toolbar"))
105+
assert "bottom_toolbar" not in enhanced[2]
106+
107+
108+
def test_build_preview_questions_adds_validate_only_for_supported_types(config):
109+
cz = PreviewCz(config)
110+
questions: list[CzQuestion] = [
111+
{"type": "input", "name": "a", "message": "A"},
112+
{"type": "confirm", "name": "b", "message": "B"},
113+
{"type": "list", "name": "c", "message": "C", "choices": []},
114+
]
115+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=3)
116+
117+
assert callable(enhanced[0].get("validate"))
118+
assert "validate" not in enhanced[1]
119+
assert "validate" not in enhanced[2]
120+
121+
122+
def test_toolbar_uses_current_buffer_text_and_subject_builder(
123+
monkeypatch: pytest.MonkeyPatch,
124+
config,
125+
):
126+
cz = PreviewCz(config)
127+
128+
class DummyBuffer:
129+
text = "buffered"
130+
131+
class DummyLayout:
132+
current_buffer = DummyBuffer()
133+
134+
class DummyApp:
135+
layout = DummyLayout()
136+
137+
monkeypatch.setattr("commitizen.preview_questions.get_app", lambda: DummyApp())
138+
139+
questions: list[CzQuestion] = [
140+
{"type": "input", "name": "subject", "message": "Subject"},
141+
]
142+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
143+
toolbar_text = enhanced[0]["bottom_toolbar"]()
144+
145+
# DummyCz.message uses subject from current buffer text via subject_builder
146+
assert "buffered" in toolbar_text
147+
148+
149+
def test_get_current_buffer_text_on_get_app_exception_returns_empty(
150+
monkeypatch: pytest.MonkeyPatch,
151+
config,
152+
):
153+
cz = PreviewCz(config)
154+
monkeypatch.setattr("commitizen.preview_questions.get_app", lambda: 1 / 0)
155+
156+
questions: list[CzQuestion] = [
157+
{"type": "input", "name": "subject", "message": "Subject"},
158+
]
159+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
160+
toolbar_text = enhanced[0]["bottom_toolbar"]()
161+
162+
# With empty buffer text, subject becomes empty -> toolbar still contains counter line
163+
assert toolbar_text.splitlines()[-1].strip().endswith("chars")
164+
165+
166+
def test_subject_builder_applies_field_filter_and_handles_filter_exception(
167+
monkeypatch: pytest.MonkeyPatch,
168+
config,
169+
):
170+
cz = PreviewCz(config)
171+
172+
def ok_filter(raw: str) -> str:
173+
return raw.strip()
174+
175+
def boom_filter(_raw: str) -> str:
176+
raise RuntimeError("boom")
177+
178+
class DummyBuffer:
179+
text = " SCOPE "
180+
181+
class DummyLayout:
182+
current_buffer = DummyBuffer()
183+
184+
class DummyApp:
185+
layout = DummyLayout()
186+
187+
questions: list[CzQuestion] = [
188+
{"type": "input", "name": "subject", "message": "Subject", "filter": ok_filter},
189+
{"type": "input", "name": "scope", "message": "Scope", "filter": boom_filter},
190+
]
191+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
192+
193+
# Update state for 'subject' (ok filter)
194+
enhanced[0]["filter"](" hi ")
195+
# When rendering toolbar for current field 'scope', subject_builder will apply the
196+
# field filter to the current buffer text; filter exceptions must fallback to raw.
197+
monkeypatch.setattr("commitizen.preview_questions.get_app", lambda: DummyApp())
198+
199+
# Render toolbar for scope and ensure it still includes subject, and scope raw is used
200+
toolbar_text = enhanced[1]["bottom_toolbar"]()
201+
assert "hi" in toolbar_text
202+
assert cz.calls[-1]["scope"] == " SCOPE "
203+
204+
205+
def test_subject_builder_handles_cz_message_exception_returns_empty(
206+
monkeypatch: pytest.MonkeyPatch,
207+
config,
208+
mocker,
209+
):
210+
class BoomCz(PreviewCz):
211+
def message(self, _answers: Mapping[str, Any]) -> str:
212+
raise RuntimeError("boom")
213+
214+
cz = BoomCz(config)
215+
216+
questions: list[CzQuestion] = [
217+
{"type": "input", "name": "subject", "message": "Subject"},
218+
]
219+
enhanced = build_preview_questions(cz, questions, enabled=True, max_length=50)
220+
221+
# Force deterministic terminal width to avoid wrap dependence
222+
monkeypatch.setattr(
223+
"commitizen.interactive_preview.get_terminal_size",
224+
lambda: mocker.Mock(columns=80),
225+
)
226+
toolbar_text = enhanced[0]["bottom_toolbar"]()
227+
assert toolbar_text.splitlines()[0] == ""

0 commit comments

Comments
 (0)