diff --git a/AddonManagerTest/gui/test_package_list_filter.py b/AddonManagerTest/gui/test_package_list_filter.py new file mode 100644 index 00000000..e4c37372 --- /dev/null +++ b/AddonManagerTest/gui/test_package_list_filter.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2026 The FreeCAD project association AISBL * +# * * +# * This file is part of the FreeCAD Addon Manager. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +import unittest + +from Addon import Addon +from package_list import PackageListFilter, PackageListItemModel + + +class TestPackageListFilter(unittest.TestCase): + + def setUp(self): + self.model = PackageListItemModel() + self.model.repos = [] + self.item_filter = PackageListFilter() + self.item_filter.setSourceModel(self.model) + + def test_text_filter_handles_empty_metadata_fields(self): + addon = Addon("AddonWithoutMetadata", status=Addon.Status.NOT_INSTALLED) + addon.description = None + addon.tags = set() + addon.macro = None + self.model.append_item(addon) + + self.item_filter.setFilterRegularExpression("SheetMetal") + + self.assertFalse(self.item_filter.filterAcceptsRow(0)) + + def test_text_filter_matches_name_when_description_missing(self): + addon = Addon("SheetMetal", status=Addon.Status.NOT_INSTALLED) + addon.description = None + addon.tags = set() + addon.macro = None + self.model.append_item(addon) + + self.item_filter.setFilterRegularExpression("sheetmetal") + + self.assertTrue(self.item_filter.filterAcceptsRow(0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/TestAddonManagerGui.py b/TestAddonManagerGui.py index d55f2260..688a257a 100644 --- a/TestAddonManagerGui.py +++ b/TestAddonManagerGui.py @@ -41,6 +41,9 @@ from AddonManagerTest.gui.test_uninstaller_gui import ( TestUninstallerGUI as AddonManagerTestUninstallerGUI, ) +from AddonManagerTest.gui.test_package_list_filter import ( + TestPackageListFilter as AddonManagerTestPackageListFilter, +) class TestListTerminator: @@ -55,6 +58,7 @@ class TestListTerminator: AddonManagerTestMacroInstallerGui, AddonManagerTestUpdateAllGui, AddonManagerTestUninstallerGUI, + AddonManagerTestPackageListFilter, TestListTerminator, # Needed to prevent the last test from running twice ] for test in loaded_gui_tests: diff --git a/package_list.py b/package_list.py index c82c30ad..fa436978 100644 --- a/package_list.py +++ b/package_list.py @@ -585,6 +585,19 @@ def __init__(self): self.hide_unlicensed = False self.hide_newer_freecad_required = False + @staticmethod + def _safe_filter_strings(data): + values = [ + getattr(data, "name", None), + getattr(data, "display_name", None), + getattr(data, "description", None), + ] + macro = getattr(data, "macro", None) + if macro: + values.append(getattr(macro, "comment", None)) + values.extend(getattr(data, "tags", []) or []) + return [str(value) for value in values if value] + def setPackageFilter( self, package_type: int ) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs, 4=Bundles, 5=Other @@ -690,34 +703,21 @@ def filterAcceptsRow(self, row, _parent=QtCore.QModelIndex()): # ) return False - name = data.display_name - desc = data.description + filter_strings = self._safe_filter_strings(data) if hasattr(self, "filterRegularExpression"): # Added in Qt 5.12 re = self.filterRegularExpression() if re.isValid(): re.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption) - if re.match(name).hasMatch(): - return True - if re.match(desc).hasMatch(): - return True - if data.macro and data.macro.comment and re.match(data.macro.comment).hasMatch(): - return True - for tag in data.tags: - if re.match(tag).hasMatch(): + for value in filter_strings: + if re.match(value).hasMatch(): return True return False # Only get here for Qt < 5.12 re = self.filterRegExp() if re.isValid(): re.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - if re.indexIn(name) != -1: - return True - if re.indexIn(desc) != -1: - return True - if data.macro and data.macro.comment and re.indexIn(data.macro.comment) != -1: - return True - for tag in data.tags: - if re.indexIn(tag) != -1: + for value in filter_strings: + if re.indexIn(value) != -1: return True return False