From 30a202c8335494da9263403f597bfbcc68dc93e7 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Tue, 6 Oct 2026 22:59:31 +0800 Subject: [PATCH] Fix startup settings window flash Signed-off-by: Loren Eteval --- Furious/Window/SettingsPage.py | 3 +- tests/README.md | 2 +- tests/test_ui_behavior.py | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/Furious/Window/SettingsPage.py b/Furious/Window/SettingsPage.py index 60f0e85..605f133 100644 --- a/Furious/Window/SettingsPage.py +++ b/Furious/Window/SettingsPage.py @@ -777,7 +777,8 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): self.addAction(self.findAction) self.generalSection = _SettingsSection(_('General')) - self.tunSection = _SettingsSection('TUN', translatable=False) + # Visibility is set before layout adoption; keep the section a child. + self.tunSection = _SettingsSection('TUN', translatable=False, parent=self) self.connectionSection = _SettingsSection(_('Connection and Interface')) self.applicationSection = _SettingsSection(_('Application')) self.pluginSettingsTitleLabel = AppQLabel(_('Plugin Settings')) diff --git a/tests/README.md b/tests/README.md index 3485d4b..56aafa5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -114,7 +114,7 @@ worker. Choose tests by the changed contract rather than by filename alone. | [test_main_window_geometry.py](test_main_window_geometry.py) | First-show lifecycle, restored/default/legacy geometry, main/routing window reuse, session-only navigation state, protection of never-shown windows' saved geometry. | | [test_dialog_geometry.py](test_dialog_geometry.py) | Dialog show/open/exec preparation, sizing/centering, failure cleanup, specialized message-box geometry and transient destruction. | | [test_layout_matrix.py](test_layout_matrix.py) | Navigation and message-box layouts in fresh processes at scale factors 1, 1.25, 1.5, and 2, under both themes. | -| [test_ui_behavior.py](test_ui_behavior.py) | Translation and editor mappings, settings organization/search/platform gates, stable server/routing moves, real QR rendering/decoding, incremental log filtering/tail behavior, connected-routing change notices, message boxes and connection-error copy/log navigation, shared Home/tray state. | +| [test_ui_behavior.py](test_ui_behavior.py) | Translation and editor mappings, settings construction visibility/organization/search/platform gates, stable server/routing moves, real QR rendering/decoding, incremental log filtering/tail behavior, connected-routing change notices, message boxes and connection-error copy/log navigation, shared Home/tray state. | | [test_qt_interactions.py](test_qt_interactions.py) | Real keyboard/mouse/focus and proxy mapping, scoped shortcuts, sorting/selection, stable subscription deletion confirmations, debounced Home search, Tests-menu selection color, shared settings, batched profile mutation and cancellation. | | [test_qr_export_scalability.py](test_qr_export_scalability.py) | Production capture cap, immediate single export, incremental yielding, failure/cancel/close paths, immutable snapshots, window-owned state destruction. | | [test_stylesheet_states.py](test_stylesheet_states.py) | Targeted rendering/alpha/geometry assertions for table/list insets, popup corners, clear buttons, focus/disabled states, and stylesheet composition. | diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index 1c8ad6c..9b6b4fb 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -449,6 +449,70 @@ class SettingsPageOrganizationTest(unittest.TestCase): return page, callbacks + def testConstructionDoesNotShowTopLevelSettingsWidgets(self): + """Never expose a section as a separate window before layout adoption.""" + + class ShowObserver(QtCore.QObject): + def __init__(self, parent): + super().__init__(parent) + + self.shownWindows = [] + + def eventFilter(self, watched, event): + if ( + event.type() == QtCore.QEvent.Type.Show + and isinstance(watched, QWidget) + and watched.isWindow() + ): + self.shownWindows.append(type(watched).__name__) + + return False + + cases = ( + ('Windows', ''), + ('Darwin', ''), + ('Linux', ''), + ('Linux', 'io.github.LorenEteval.Furious'), + ) + app = application() + + for platform, flatpakID in cases: + with self.subTest(platform=platform, flatpakID=flatpakID): + observer = ShowObserver(app) + page = None + app.installEventFilter(observer) + + with mock.patch('sys.excepthook') as callbackExceptionHook: + try: + page, _callbacks = self.buildPage( + platform=platform, flatpakID=flatpakID + ) + + self.assertEqual(observer.shownWindows, []) + self.assertFalse(page.isVisible()) + self.assertFalse(page.tunSection.isWindow()) + self.assertEqual(page.tunSection.isHidden(), bool(flatpakID)) + self.assertEqual( + page.tunSection in page._searchSections, not flatpakID + ) + + page.show() + processQtEvents() + + self.assertTrue(page.isVisible()) + self.assertEqual(page.tunSection.isVisible(), not flatpakID) + finally: + app.removeEventFilter(observer) + + if page is not None: + page.close() + page.deleteLater() + + observer.deleteLater() + processQtEvents() + + callbackExceptionHook.assert_not_called() + def testSearchMatchesDescriptionsAndSectionsWithoutApplyingSettings(self): with isolatedSettings(): AppSettings.set('Language', 'EN')