Skip to content

Commit 10c5560

Browse files
committed
Addon Manager: Add GUI tests for branch change dialog
1 parent 4e9edcd commit 10c5560

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
# ***************************************************************************
3+
# * *
4+
# * Copyright (c) 2025 The FreeCAD Project Association AISBL *
5+
# * *
6+
# * This file is part of FreeCAD. *
7+
# * *
8+
# * FreeCAD is free software: you can redistribute it and/or modify it *
9+
# * under the terms of the GNU Lesser General Public License as *
10+
# * published by the Free Software Foundation, either version 2.1 of the *
11+
# * License, or (at your option) any later version. *
12+
# * *
13+
# * FreeCAD is distributed in the hope that it will be useful, but *
14+
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
15+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16+
# * Lesser General Public License for more details. *
17+
# * *
18+
# * You should have received a copy of the GNU Lesser General Public *
19+
# * License along with FreeCAD. If not, see *
20+
# * <https://www.gnu.org/licenses/>. *
21+
# * *
22+
# ***************************************************************************
23+
24+
import sys
25+
import unittest
26+
from unittest.mock import patch, Mock, MagicMock
27+
28+
# So that when run standalone, the Addon Manager classes imported below are available
29+
sys.path.append("../..")
30+
31+
from AddonManagerTest.gui.gui_mocks import DialogWatcher, DialogInteractor, AsynchronousMonitor
32+
33+
from change_branch import ChangeBranchDialog
34+
35+
from addonmanager_freecad_interface import translate
36+
from addonmanager_git import GitFailed
37+
38+
try:
39+
from PySide import QtCore, QtWidgets
40+
except ImportError:
41+
try:
42+
from PySide6 import QtCore, QtWidgets
43+
except ImportError:
44+
from PySide2 import QtCore, QtWidgets
45+
46+
47+
class MockFilter(QtCore.QSortFilterProxyModel):
48+
def mapToSource(self, something):
49+
return something
50+
51+
52+
class MockChangeBranchDialogModel(QtCore.QAbstractTableModel):
53+
54+
branches = [
55+
{"ref_name": "ref1", "upstream": "us1"},
56+
{"ref_name": "ref2", "upstream": "us2"},
57+
{"ref_name": "ref3", "upstream": "us3"},
58+
]
59+
current_branch = "ref1"
60+
DataSortRole = QtCore.Qt.UserRole
61+
RefAccessRole = QtCore.Qt.UserRole + 1
62+
63+
def __init__(self, _: str, parent=None) -> None:
64+
super().__init__(parent)
65+
66+
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
67+
if parent.isValid():
68+
return 0
69+
return len(self.branches)
70+
71+
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
72+
if parent.isValid():
73+
return 0
74+
return 3 # Local name, remote name, date
75+
76+
def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
77+
if not index.isValid():
78+
return None
79+
row = index.row()
80+
column = index.column()
81+
if role == QtCore.Qt.DisplayRole:
82+
if column == 2:
83+
return "date"
84+
elif column == 0:
85+
return "ref_name"
86+
elif column == 1:
87+
return "upstream"
88+
else:
89+
return None
90+
elif role == MockChangeBranchDialogModel.DataSortRole:
91+
return None
92+
elif role == MockChangeBranchDialogModel.RefAccessRole:
93+
return self.branches[row]
94+
95+
def headerData(
96+
self,
97+
section: int,
98+
orientation: QtCore.Qt.Orientation,
99+
role: int = QtCore.Qt.DisplayRole,
100+
):
101+
if orientation == QtCore.Qt.Vertical:
102+
return None
103+
if role != QtCore.Qt.DisplayRole:
104+
return None
105+
if section == 0:
106+
return "Local"
107+
if section == 1:
108+
return "Remote tracking"
109+
elif section == 2:
110+
return "Last Updated"
111+
else:
112+
return None
113+
114+
def currentBranch(self) -> str:
115+
return self.current_branch
116+
117+
118+
class TestChangeBranchGui(unittest.TestCase):
119+
120+
MODULE = "test_change_branch" # file name without extension
121+
122+
def setUp(self):
123+
pass
124+
125+
def tearDown(self):
126+
pass
127+
128+
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
129+
@patch("change_branch.initialize_git", new=Mock(return_value=None))
130+
def test_no_git(self):
131+
# Arrange
132+
gui = ChangeBranchDialog("/some/path")
133+
ref = {"ref_name": "foo/bar", "upstream": "us1"}
134+
dialog_watcher = DialogWatcher(
135+
translate("AddonsInstaller", "Cannot find git"),
136+
QtWidgets.QDialogButtonBox.Ok,
137+
)
138+
139+
# Act
140+
gui._change_branch("/foo/bar/baz", ref)
141+
142+
# Assert
143+
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
144+
145+
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
146+
@patch("change_branch.initialize_git")
147+
def test_git_failed(self, init_git: MagicMock):
148+
# Arrange
149+
git_manager = MagicMock()
150+
git_manager.checkout = MagicMock()
151+
git_manager.checkout.side_effect = GitFailed()
152+
init_git.return_value = git_manager
153+
gui = ChangeBranchDialog("/some/path")
154+
ref = {"ref_name": "foo/bar", "upstream": "us1"}
155+
dialog_watcher = DialogWatcher(
156+
translate("AddonsInstaller", "git operation failed"),
157+
QtWidgets.QDialogButtonBox.Ok,
158+
)
159+
160+
# Act
161+
gui._change_branch("/foo/bar/baz", ref)
162+
163+
# Assert
164+
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
165+
166+
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
167+
@patch("change_branch.initialize_git", new=MagicMock)
168+
def test_branch_change_succeeded(self):
169+
# If nothing gets thrown, then the process is assumed to have worked, and the appropriate
170+
# signal is emitted.
171+
172+
# Arrange
173+
gui = ChangeBranchDialog("/some/path")
174+
ref = {"ref_name": "foo/bar", "upstream": "us1"}
175+
monitor = AsynchronousMonitor(gui.branch_changed)
176+
177+
# Act
178+
gui._change_branch("/foo/bar/baz", ref)
179+
180+
# Assert
181+
monitor.wait_for_at_most(10) # Should be effectively instantaneous
182+
self.assertTrue(monitor.good())
183+
184+
@patch("change_branch.ChangeBranchDialogFilter", new=MockFilter)
185+
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
186+
@patch("change_branch.initialize_git", new=MagicMock)
187+
def test_warning_is_shown_when_dialog_is_accepted(self):
188+
# Arrange
189+
gui = ChangeBranchDialog("/some/path")
190+
gui.ui.exec = MagicMock()
191+
gui.ui.exec.return_value = QtWidgets.QDialog.Accepted
192+
gui.ui.tableView.selectedIndexes = MagicMock()
193+
gui.ui.tableView.selectedIndexes.return_value = [MagicMock()]
194+
gui.ui.tableView.selectedIndexes.return_value[0].isValid = MagicMock()
195+
gui.ui.tableView.selectedIndexes.return_value[0].isValid.return_value = True
196+
dialog_watcher = DialogWatcher(
197+
translate("AddonsInstaller", "DANGER: Developer feature"),
198+
QtWidgets.QDialogButtonBox.Cancel,
199+
)
200+
201+
# Act
202+
gui.exec()
203+
204+
# Assert
205+
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
206+
207+
208+
if __name__ == "__main__":
209+
app = QtWidgets.QApplication(sys.argv)
210+
QtCore.QTimer.singleShot(0, unittest.main)
211+
if hasattr(app, "exec"):
212+
app.exec() # PySide6
213+
else:
214+
app.exec_() # PySide2

0 commit comments

Comments
 (0)