import bouquin.bug_report_dialog as bugmod from bouquin.bug_report_dialog import BugReportDialog from bouquin import strings from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QTextCursor def test_bug_report_truncates_text_to_max_chars(qtbot): dlg = BugReportDialog() qtbot.addWidget(dlg) dlg.show() max_chars = getattr(dlg, "MAX_CHARS", 5000) # Make a string longer than the allowed maximum long_text = "x" * (max_chars + 50) # Setting the text should trigger textChanged -> _enforce_max_length dlg.text_edit.setPlainText(long_text) # Let Qt process the signal/slot if needed qtbot.wait(10) current = dlg.text_edit.toPlainText() assert len(current) == max_chars assert current == long_text[:max_chars] def test_bug_report_allows_up_to_max_chars_unchanged(qtbot): dlg = BugReportDialog() qtbot.addWidget(dlg) dlg.show() max_chars = getattr(dlg, "MAX_CHARS", 5000) exact_text = "y" * max_chars dlg.text_edit.setPlainText(exact_text) qtbot.wait(10) current = dlg.text_edit.toPlainText() # Should not be trimmed if it's exactly the limit assert len(current) == max_chars assert current == exact_text def test_bug_report_send_success_201_shows_info_and_accepts(qtbot, monkeypatch): dlg = BugReportDialog() qtbot.addWidget(dlg) dlg.show() # Non-empty message so we don't hit the "empty" warning branch dlg.text_edit.setPlainText("Hello, something broke.") qtbot.wait(10) # Make version() deterministic def fake_version(pkg_name): assert pkg_name == "bouquin" return "1.2.3" monkeypatch.setattr( bugmod.importlib.metadata, "version", fake_version, raising=True ) # Capture the POST call and fake a 201 Created response calls = {} class DummyResp: status_code = 201 def fake_post(url, json=None, timeout=None): calls["url"] = url calls["json"] = json calls["timeout"] = timeout return DummyResp() monkeypatch.setattr(bugmod.requests, "post", fake_post, raising=True) # Capture information / critical message boxes info_called = {} crit_called = {} def fake_info(parent, title, text, *a, **k): info_called["title"] = title info_called["text"] = str(text) return 0 def fake_critical(parent, title, text, *a, **k): crit_called["title"] = title crit_called["text"] = str(text) return 0 monkeypatch.setattr( bugmod.QMessageBox, "information", staticmethod(fake_info), raising=True ) monkeypatch.setattr( bugmod.QMessageBox, "critical", staticmethod(fake_critical), raising=True ) # Don't actually close the dialog in the test; just record that accept() was called accepted = {} def fake_accept(): accepted["called"] = True dlg.accept = fake_accept # Call the send logic directly dlg._send() # --- Assertions --------------------------------------------------------- # POST was called with the expected URL and JSON payload assert calls["url"] == f"{bugmod.BUG_REPORT_HOST}/{bugmod.ROUTE}" assert calls["json"]["message"] == "Hello, something broke." assert calls["json"]["version"] == "1.2.3" # No attachment fields expected any more # Success path: information dialog shown, critical not shown assert "title" in info_called assert "text" in info_called assert crit_called == {} # Dialog accepted assert accepted.get("called") is True def test_bug_report_send_failure_non_201_shows_critical_and_not_accepted( qtbot, monkeypatch ): dlg = BugReportDialog() qtbot.addWidget(dlg) dlg.show() dlg.text_edit.setPlainText("Broken again.") qtbot.wait(10) # Stub version() again monkeypatch.setattr( bugmod.importlib.metadata, "version", lambda name: "9.9.9", raising=True, ) # Fake a non-201 response (e.g. 500) calls = {} class DummyResp: status_code = 500 def fake_post(url, json=None, timeout=None): calls["url"] = url calls["json"] = json calls["timeout"] = timeout return DummyResp() monkeypatch.setattr(bugmod.requests, "post", fake_post, raising=True) info_called = {} crit_called = {} def fake_info(parent, title, text, *a, **k): info_called["title"] = title info_called["text"] = str(text) return 0 def fake_critical(parent, title, text, *a, **k): crit_called["title"] = title crit_called["text"] = str(text) return 0 monkeypatch.setattr( bugmod.QMessageBox, "information", staticmethod(fake_info), raising=True ) monkeypatch.setattr( bugmod.QMessageBox, "critical", staticmethod(fake_critical), raising=True ) accepted = {} def fake_accept(): accepted["called"] = True dlg.accept = fake_accept dlg._send() # POST still called with JSON payload assert calls["url"] == f"{bugmod.BUG_REPORT_HOST}/{bugmod.ROUTE}" assert calls["json"]["message"] == "Broken again." assert calls["json"]["version"] == "9.9.9" # Failure path: critical dialog shown, information not shown assert crit_called # non-empty assert info_called == {} # Dialog should NOT be accepted on failure assert accepted.get("called") is not True def test_bug_report_dialog_text_limit_clamps_cursor(qtbot): """Test that cursor position is clamped when text exceeds limit.""" strings.load_strings("en") dialog = BugReportDialog() qtbot.addWidget(dialog) dialog.show() # Set text that exceeds MAX_CHARS max_chars = dialog.MAX_CHARS long_text = "A" * (max_chars + 100) # Set text and move cursor to end dialog.text_edit.setPlainText(long_text) dialog.text_edit.moveCursor(QTextCursor.MoveOperation.End) # Text should be truncated assert len(dialog.text_edit.toPlainText()) == max_chars # Cursor should be clamped to max position final_cursor = dialog.text_edit.textCursor() assert final_cursor.position() <= max_chars def test_bug_report_dialog_empty_text_shows_warning(qtbot, monkeypatch): """Test that sending empty report shows warning.""" strings.load_strings("en") dialog = BugReportDialog() qtbot.addWidget(dialog) dialog.show() # Clear any text dialog.text_edit.clear() warning_shown = {"shown": False} def mock_warning(*args): warning_shown["shown"] = True monkeypatch.setattr(QMessageBox, "warning", mock_warning) # Try to send empty report dialog._send() assert warning_shown["shown"] def test_bug_report_dialog_whitespace_only_shows_warning(qtbot, monkeypatch): """Test that sending whitespace-only report shows warning.""" strings.load_strings("en") dialog = BugReportDialog() qtbot.addWidget(dialog) dialog.show() # Set whitespace only dialog.text_edit.setPlainText(" \n\n \t\t ") warning_shown = {"shown": False} def mock_warning(*args): warning_shown["shown"] = True monkeypatch.setattr(QMessageBox, "warning", mock_warning) dialog._send() assert warning_shown["shown"] def test_bug_report_dialog_network_error(qtbot, monkeypatch): """Test handling network error during send.""" strings.load_strings("en") dialog = BugReportDialog() qtbot.addWidget(dialog) dialog.show() dialog.text_edit.setPlainText("Test bug report") # Mock requests.post to raise exception import requests def mock_post(*args, **kwargs): raise requests.exceptions.ConnectionError("Network error") monkeypatch.setattr(requests, "post", mock_post) critical_shown = {"shown": False} def mock_critical(*args): critical_shown["shown"] = True monkeypatch.setattr(QMessageBox, "critical", mock_critical) dialog._send() assert critical_shown["shown"] def test_bug_report_dialog_timeout_error(qtbot, monkeypatch): """Test handling timeout error during send.""" strings.load_strings("en") dialog = BugReportDialog() qtbot.addWidget(dialog) dialog.show() dialog.text_edit.setPlainText("Test bug report") # Mock requests.post to raise timeout import requests def mock_post(*args, **kwargs): raise requests.exceptions.Timeout("Request timed out") monkeypatch.setattr(requests, "post", mock_post) critical_shown = {"shown": False} def mock_critical(*args): critical_shown["shown"] = True monkeypatch.setattr(QMessageBox, "critical", mock_critical) dialog._send() assert critical_shown["shown"]