dbaccess/qa/uitest/edit_field/tdf75509.py                  |   86 ++++----
 dbaccess/qa/uitest/query/insert_relation.py                |  127 ++++++-------
 dbaccess/qa/uitest/query/tdf99619_create_join_undo_redo.py |   90 ++++-----
 uitest/uitest/test.py                                      |   32 +++
 4 files changed, 175 insertions(+), 160 deletions(-)

New commits:
commit 70e001ae2da693815a390dddf9cafe8c54bbf1f4
Author:     Neil Roberts <[email protected]>
AuthorDate: Fri Nov 21 10:04:47 2025 +0100
Commit:     Noel Grandin <[email protected]>
CommitDate: Sat Nov 22 19:37:57 2025 +0100

    UITest: Add a method to open a subcomponent through a command
    
    The Base UI tests all need to open a window that operates on the same
    database, such as opening the table editor etc. There seems to be a few
    race conditions related to opening sub windows that occasionally make
    the tests fail so the idea with this patch is to make a central place in
    test.py to add code to handle opening the window and closing it when
    finished. That way we can add any synchronisation needed in a central
    place.
    
    For getting access to the new frame, the three current tests were all
    trying to handle this in different ways. insert_relation waits for the
    OnSubComponentOpened event, create_join_undo_redo was waiting until the
    current focus window changes and the edit_field test was just crossing
    its fingers and hoping that the timing worked. This last one is the
    likely culprit of the following Jenkins build error for the edit_field
    test:
    
    https://ci.libreoffice.org/job/gerrit_linux_clang_dbgutil/193148/
    
    The new helper method always listens for OnSubComponentOpened and then
    extracts the new frame from that event.
    
    When closing the window the shared code now always calls
    waitUntilAllIdlesDispatched. .uno:CloseWin ends up being executing
    asynchronously on the main thread which means that the later call to
    .uno:CloseDoc can happen simultaneously. If --oneprocess is not used
    then it will even end up directly calling dispose on the main frame
    instead of calling CloseDoc. I haven’t been able to replicate the crash
    locally, but is seems like this might be related to crashes like the one
    below:
    
    https://ci.libreoffice.org/job/gerrit_linux_clang_dbgutil/193204/
    
    Change-Id: Iab0a46c3ca9f8c9eb9ac7f4fecb07f633bd0b312
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/194350
    Reviewed-by: Noel Grandin <[email protected]>
    Tested-by: Jenkins

diff --git a/dbaccess/qa/uitest/edit_field/tdf75509.py 
b/dbaccess/qa/uitest/edit_field/tdf75509.py
index 4fb3c125de7a..3fc1957ff07c 100644
--- a/dbaccess/qa/uitest/edit_field/tdf75509.py
+++ b/dbaccess/qa/uitest/edit_field/tdf75509.py
@@ -30,68 +30,62 @@ class tdf75509(UITestCase):
             # does the trick
             self.xUITest.executeCommand(".uno:SelectAll")
 
-            self.xUITest.executeCommand(".uno:DBTableEdit")
+            with 
self.ui_test.open_subcomponent_through_command(".uno:DBTableEdit") as 
xTableFrame:
+                xTableWindow = 
self.xUITest.getWindow(xTableFrame.getContainerWindow())
 
-            xTableWindow = self.xUITest.getTopFocusWindow()
+                xTableEditor = xTableWindow.getChild("DBTableEditor")
 
-            xTableEditor = xTableWindow.getChild("DBTableEditor")
+                # Select the “FrenchField” row
+                xTableEditor.executeAction("TYPE", 
mkPropertyValues({"KEYCODE": "DOWN"}))
 
-            # Select the “FrenchField” row
-            xTableEditor.executeAction("TYPE", mkPropertyValues({"KEYCODE": 
"DOWN"}))
+                # Type a value with the comma decimal separator into the 
default value field
+                xDefaultValue = xTableWindow.getChild("DefaultValue")
 
-            # Type a value with the comma decimal separator into the default 
value field
-            xDefaultValue = xTableWindow.getChild("DefaultValue")
+                xDefaultValue.executeAction("FOCUS", tuple())
+                xDefaultValue.executeAction("SET", mkPropertyValues({"TEXT": 
"3,14"}))
 
-            xDefaultValue.executeAction("FOCUS", tuple())
-            xDefaultValue.executeAction("SET", mkPropertyValues({"TEXT": 
"3,14"}))
+                # Focus something else so that the example text will get 
updated
+                xTableEditor.executeAction("FOCUS", tuple())
 
-            # Focus something else so that the example text will get updated
-            xTableEditor.executeAction("FOCUS", tuple())
+                # The example format should be updated to reflect the default 
value
+                xFormatText = xTableWindow.getChild("FormatText")
+                self.assertEqual(get_state_as_dict(xFormatText)["Text"], 
"3,14")
 
-            # The example format should be updated to reflect the default value
-            xFormatText = xTableWindow.getChild("FormatText")
-            self.assertEqual(get_state_as_dict(xFormatText)["Text"], "3,14")
+                # Select the “EnglishField"
+                xTableEditor.executeAction("TYPE", 
mkPropertyValues({"KEYCODE": "DOWN"}))
 
-            # Select the “EnglishField"
-            xTableEditor.executeAction("TYPE", mkPropertyValues({"KEYCODE": 
"DOWN"}))
+                # Perform the same tests with the “.” decimal separator
+                xDefaultValue.executeAction("FOCUS", tuple())
+                xDefaultValue.executeAction("SET", mkPropertyValues({"TEXT": 
"2.72"}))
+                xTableEditor.executeAction("FOCUS", tuple())
+                self.assertEqual(get_state_as_dict(xFormatText)["Text"], 
"2.72")
 
-            # Perform the same tests with the “.” decimal separator
-            xDefaultValue.executeAction("FOCUS", tuple())
-            xDefaultValue.executeAction("SET", mkPropertyValues({"TEXT": 
"2.72"}))
-            xTableEditor.executeAction("FOCUS", tuple())
-            self.assertEqual(get_state_as_dict(xFormatText)["Text"], "2.72")
+                # Save the table (ie, just the table, not the actual database 
file to disk)
+                self.xUITest.executeCommandForProvider(".uno:Save", 
xTableFrame)
 
-            # Save the table (ie, just the table, not the actual database file 
to disk)
-            self.xUITest.executeCommandForProvider(
-                ".uno:Save",
-                self.ui_test.get_desktop().getCurrentFrame())
+            with 
self.ui_test.open_subcomponent_through_command(".uno:DBTableOpen") as 
xTableFrame:
+                xTableWindow = 
self.xUITest.getWindow(xTableFrame.getContainerWindow())
 
-            self.xUITest.executeCommand(".uno:DBTableOpen")
+                # Focus the “FrenchField” input in the table
+                xGrid = xTableWindow.getChild("DBGrid")
+                xGrid.executeAction("FOCUS", tuple())
+                xGrid.executeAction("TYPE", mkPropertyValues({"KEYCODE": 
"TAB"}))
 
-            xTableWindow = self.xUITest.getTopFocusWindow()
+                # It should have the default value with a comma separator
+                xEdit = self.xUITest.getFocusWindow()
+                self.assertEqual(get_state_as_dict(xEdit)["Text"], "3,14")
 
-            # Focus the “FrenchField” input in the table
-            xGrid = xTableWindow.getChild("DBGrid")
-            xGrid.executeAction("FOCUS", tuple())
-            xGrid.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))
+                # Focus the “EnglishField” input in the table
+                xGrid.executeAction("TYPE", mkPropertyValues({"KEYCODE": 
"TAB"}))
 
-            # It should have the default value with a comma separator
-            xEdit = self.xUITest.getFocusWindow()
-            self.assertEqual(get_state_as_dict(xEdit)["Text"], "3,14")
+                # It should have the default value with a dot separator
+                xEdit = self.xUITest.getFocusWindow()
+                self.assertEqual(get_state_as_dict(xEdit)["Text"], "2.72")
 
-            # Focus the “EnglishField” input in the table
-            xGrid.executeAction("TYPE", mkPropertyValues({"KEYCODE": "TAB"}))
+                # Modify the field so that the we can save the record
+                xEdit.executeAction("SET", mkPropertyValues({"TEXT": "2.71"}))
 
-            # It should have the default value with a dot separator
-            xEdit = self.xUITest.getFocusWindow()
-            self.assertEqual(get_state_as_dict(xEdit)["Text"], "2.72")
-
-            # Modify the field so that the we can save the record
-            xEdit.executeAction("SET", mkPropertyValues({"TEXT": "2.71"}))
-
-            self.xUITest.executeCommandForProvider(
-                ".uno:RecSave",
-                self.ui_test.get_desktop().getCurrentFrame())
+                self.xUITest.executeCommandForProvider(".uno:RecSave", 
xTableFrame)
 
             # Check that the default values actually entered the database
             xDbController = 
self.ui_test.get_desktop().getActiveFrame().getController()
diff --git a/dbaccess/qa/uitest/query/insert_relation.py 
b/dbaccess/qa/uitest/query/insert_relation.py
index d6d291bea887..9ee0251580fc 100644
--- a/dbaccess/qa/uitest/query/insert_relation.py
+++ b/dbaccess/qa/uitest/query/insert_relation.py
@@ -22,82 +22,81 @@ class InsertRelation(UITestCase):
         with self.ui_test.create_db_in_start_center() as xDocument:
             # Create three tables using the design view
             for table_num in range(3):
-                with EventListener(self.xContext, "OnSubComponentOpened") as 
event:
-                    self.xUITest.executeCommand(".uno:DBNewTable")
-                    while not event.executed:
-                        time.sleep(DEFAULT_SLEEP)
-
-                # Press TAB in the table editor and then type in a field name
-                xTableWindow = self.xUITest.getTopFocusWindow()
-                xTableEditor = xTableWindow.getChild("DBTableEditor")
-                xTableEditor.executeAction("TYPE", 
mkPropertyValues({"KEYCODE": "TAB"}))
-                self.xUITest.getFocusWindow().executeAction(
-                    "SET", mkPropertyValues({"TEXT": f"field{table_num + 1}"}))
+                with self.ui_test.open_subcomponent_through_command(
+                        ".uno:DBNewTable", close_win=False) as xTableFrame:
 
-                xTableFrame = self.ui_test.get_desktop().getCurrentFrame()
+                    # Press TAB in the table editor and then type in a field 
name
+                    xTableWindow = 
self.xUITest.getWindow(xTableFrame.getContainerWindow())
+                    xTableEditor = xTableWindow.getChild("DBTableEditor")
+                    xTableEditor.executeAction("TYPE", 
mkPropertyValues({"KEYCODE": "TAB"}))
+                    self.xUITest.getFocusWindow().executeAction(
+                        "SET", mkPropertyValues({"TEXT": f"field{table_num + 
1}"}))
 
-                # Close the window. This will open a dialog asking if we want 
to save
-                with self.ui_test.execute_blocking_action(
-                        self.xUITest.executeCommandForProvider,
-                        args=(".uno:CloseWin", xTableFrame),
-                        close_button=None) as xSaveDialog:
-                    # Choose yes. This will open another dialog asking us to 
name the table
+                    # Close the window. This will open a dialog asking if we 
want to save
                     with self.ui_test.execute_blocking_action(
-                            xSaveDialog.getChild("yes").executeAction,
-                            args=("CLICK", tuple()),
-                            close_button=None) as xNameDialog:
-                        xNameDialog.getChild("title").executeAction(
-                            "SET", mkPropertyValues({"TEXT": f"table{table_num 
+ 1}"}))
-                        # Clicking OK will open a third dialog asking if we 
want to add a primary
-                        # key
+                            self.xUITest.executeCommandForProvider,
+                            args=(".uno:CloseWin", xTableFrame),
+                            close_button=None) as xSaveDialog:
+                        # Choose yes. This will open another dialog asking us 
to name the table
                         with self.ui_test.execute_blocking_action(
-                                xNameDialog.getChild("ok").executeAction,
+                                xSaveDialog.getChild("yes").executeAction,
                                 args=("CLICK", tuple()),
-                                close_button="yes"):
-                            pass
+                                close_button=None) as xNameDialog:
+                            xNameDialog.getChild("title").executeAction(
+                                "SET", mkPropertyValues({"TEXT": 
f"table{table_num + 1}"}))
+                            # Clicking OK will open a third dialog
+                            # asking if we want to add a primary key
+                            with self.ui_test.execute_blocking_action(
+                                    xNameDialog.getChild("ok").executeAction,
+                                    args=("CLICK", tuple()),
+                                    close_button="yes"):
+                                pass
 
             # Create a new query with the design view. This will open a new 
frame for the query as
             # well as a modeless dialog to add the tables
-            with self.ui_test.execute_dialog_through_command(
-                    ".uno:DBNewQuery", close_button="close") as 
xAddTablesDialog:
-                xTableList = xAddTablesDialog.getChild("tablelist")
-                xAdd = xAddTablesDialog.getChild("add")
-                # Select and add all of the tables in turn
-                for i in range(3):
-                    xTableList.getChild(str(i)).executeAction("SELECT", 
tuple())
-                    xAdd.executeAction("CLICK", tuple())
+            with EventListener(self.xContext, "DialogExecute") as event:
+                with self.ui_test.open_subcomponent_through_command(
+                        ".uno:DBNewQuery") as xQueryFrame:
+                    while not event.executed:
+                        time.sleep(DEFAULT_SLEEP)
+                    try:
+                        xAddTablesDialog = self.xUITest.getTopFocusWindow()
+                        xTableList = xAddTablesDialog.getChild("tablelist")
+                        xAdd = xAddTablesDialog.getChild("add")
+                        # Select and add all of the tables in turn
+                        for i in range(3):
+                            
xTableList.getChild(str(i)).executeAction("SELECT", tuple())
+                            xAdd.executeAction("CLICK", tuple())
+                    finally:
+                        self.ui_test.close_dialog_through_button(
+                            xAddTablesDialog.getChild("close"), 
xAddTablesDialog)
 
-            xQueryFrame = self.ui_test.get_desktop().getCurrentFrame()
-            xQueryController = xQueryFrame.getController()
+                    xQueryController = xQueryFrame.getController()
 
-            try:
-                with self.ui_test.execute_blocking_action(
-                        self.xUITest.executeCommandForProvider,
-                        args=(".uno:DBAddRelation", xQueryController),
-                        close_button="cancel") as xDialog:
-                    xTable1 = xDialog.getChild("table1")
-                    xTable2 = xDialog.getChild("table2")
+                    with self.ui_test.execute_blocking_action(
+                            self.xUITest.executeCommandForProvider,
+                            args=(".uno:DBAddRelation", xQueryController),
+                            close_button="cancel") as xDialog:
+                        xTable1 = xDialog.getChild("table1")
+                        xTable2 = xDialog.getChild("table2")
 
-                    table2_value = 
get_state_as_dict(xTable2)["SelectEntryText"]
+                        table2_value = 
get_state_as_dict(xTable2)["SelectEntryText"]
 
-                    # Try setting table1 to the same value as table2
-                    select_by_text(xTable1, table2_value)
-                    # Make sure that it worked
-                    
self.assertEqual(get_state_as_dict(xTable1)["SelectEntryText"], table2_value)
-                    
self.assertTrue(get_state_as_dict(xTable2)["SelectEntryText"] != table2_value)
+                        # Try setting table1 to the same value as table2
+                        select_by_text(xTable1, table2_value)
+                        # Make sure that it worked
+                        
self.assertEqual(get_state_as_dict(xTable1)["SelectEntryText"],
+                                         table2_value)
+                        
self.assertTrue(get_state_as_dict(xTable2)["SelectEntryText"] !=
+                                        table2_value)
 
-                    # Try choosing all 3 tables for table1
-                    for i in range(3):
-                        table_name = f"table{i + 1}"
-                        select_by_text(xTable1, table_name)
-                        
self.assertEqual(get_state_as_dict(xTable1)["SelectEntryText"], table_name)
-                        
self.assertTrue(get_state_as_dict(xTable2)["SelectEntryText"] != table_name)
-            finally:
-                # Close the query window and answer no when it asks if we want 
to save
-                with self.ui_test.execute_blocking_action(
-                        self.xUITest.executeCommandForProvider,
-                        args=(".uno:CloseWin", xQueryFrame),
-                        close_button="no"):
-                    pass
+                        # Try choosing all 3 tables for table1
+                        for i in range(3):
+                            table_name = f"table{i + 1}"
+                            select_by_text(xTable1, table_name)
+                            
self.assertEqual(get_state_as_dict(xTable1)["SelectEntryText"],
+                                             table_name)
+                            
self.assertTrue(get_state_as_dict(xTable2)["SelectEntryText"] !=
+                                            table_name)
 
 # vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/dbaccess/qa/uitest/query/tdf99619_create_join_undo_redo.py 
b/dbaccess/qa/uitest/query/tdf99619_create_join_undo_redo.py
index 7484e4371a2f..eccaf8162c71 100644
--- a/dbaccess/qa/uitest/query/tdf99619_create_join_undo_redo.py
+++ b/dbaccess/qa/uitest/query/tdf99619_create_join_undo_redo.py
@@ -33,55 +33,45 @@ class tdf99619(UITestCase):
 
             xDbFrame = self.ui_test.get_desktop().getCurrentFrame()
 
-            self.xUITest.executeCommand(".uno:DBQueryEdit")
-
-            while True:
-                xQueryFrame = self.ui_test.get_desktop().getCurrentFrame()
-
-                if xQueryFrame != xDbFrame:
-                    break
-                time.sleep(DEFAULT_SLEEP)
-
-            xQueryController = xQueryFrame.getController()
-
-            # Add a relation via the dialog
-            with self.ui_test.execute_blocking_action(
-                    self.xUITest.executeCommandForProvider,
-                    args=(".uno:DBAddRelation", xQueryController)) as xDialog:
-
-                # Choose the two tables
-                select_by_text(xDialog.getChild("table1"), "object")
-                select_by_text(xDialog.getChild("table2"), "person")
-
-                # Set the join type
-                select_by_text(xDialog.getChild("type"), "Inner join")
-
-                # Use a natural join because it’s too difficult to manipulate 
the grid to select
-                # fields
-                xDialog.getChild("natural").executeAction("CLICK", tuple())
-
-            # Undo the join
-            self.xUITest.executeCommandForProvider(".uno:Undo", xQueryFrame)
-            # Redo the join. This is where it crashes without any fixes to the 
bug
-            self.xUITest.executeCommandForProvider(".uno:Redo", xQueryFrame)
-
-            # Save the query. This only saves the query in memory and doesn’t 
change the database
-            # file on disk
-            self.xUITest.executeCommandForProvider(".uno:Save", xQueryFrame)
-
-            # Switch to SQL mode
-            self.xUITest.executeCommandForProvider(".uno:DBChangeDesignMode",
-                                                   xQueryController)
-
-            # Get the SQL source for the query
-            xSql = self.xUITest.getTopFocusWindow().getChild("sql")
-            query = get_state_as_dict(xSql)["Text"]
-
-            # Make sure that the join is in the query
-            if "NATURAL INNER JOIN" not in query:
-                print(f"Join missing from query: {query}", file=sys.stderr)
-            self.assertTrue("NATURAL INNER JOIN" in query)
-
-            self.xUITest.executeCommandForProvider(".uno:CloseWin", 
xQueryFrame)
+            with 
self.ui_test.open_subcomponent_through_command(".uno:DBQueryEdit") as 
xQueryFrame:
+                xQueryController = xQueryFrame.getController()
+
+                # Add a relation via the dialog
+                with self.ui_test.execute_blocking_action(
+                        self.xUITest.executeCommandForProvider,
+                        args=(".uno:DBAddRelation", xQueryController)) as 
xDialog:
+
+                    # Choose the two tables
+                    select_by_text(xDialog.getChild("table1"), "object")
+                    select_by_text(xDialog.getChild("table2"), "person")
+
+                    # Set the join type
+                    select_by_text(xDialog.getChild("type"), "Inner join")
+
+                    # Use a natural join because it’s too difficult to 
manipulate the grid to select
+                    # fields
+                    xDialog.getChild("natural").executeAction("CLICK", tuple())
+
+                # Undo the join
+                self.xUITest.executeCommandForProvider(".uno:Undo", 
xQueryFrame)
+                # Redo the join. This is where it crashes without any fixes to 
the bug
+                self.xUITest.executeCommandForProvider(".uno:Redo", 
xQueryFrame)
+
+                # Save the query. This only saves the query in memory
+                # and doesn’t change the database file on disk
+                self.xUITest.executeCommandForProvider(".uno:Save", 
xQueryFrame)
+
+                # Switch to SQL mode
+                
self.xUITest.executeCommandForProvider(".uno:DBChangeDesignMode",
+                                                       xQueryController)
+
+                # Get the SQL source for the query
+                xSql = self.xUITest.getTopFocusWindow().getChild("sql")
+                query = get_state_as_dict(xSql)["Text"]
+
+                # Make sure that the join is in the query
+                if "NATURAL INNER JOIN" not in query:
+                    print(f"Join missing from query: {query}", file=sys.stderr)
+                self.assertTrue("NATURAL INNER JOIN" in query)
 
 # vim: set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/uitest/uitest/test.py b/uitest/uitest/test.py
index 505e3140ead1..883d6564df8d 100644
--- a/uitest/uitest/test.py
+++ b/uitest/uitest/test.py
@@ -168,6 +168,38 @@ class UITest(object):
             ui_object.executeAction(action, parameters)
             yield from self.wait_and_yield_dialog(event, xDialogParent, 
close_button)
 
+    # Executes a command and waits for a subcomponent event to be emitted. The 
frame from the event
+    # will be yielded. If close_win is True then .uno:CloseWin will be called 
at exit. This can be
+    # used for commands that open a new window for the same document.
+    @contextmanager
+    def open_subcomponent_through_command(self, command, printNames=False, 
close_win=True):
+        with EventListener(self._xContext, "OnSubComponentOpened", 
printNames=printNames) as event:
+            self._xUITest.executeCommand(command)
+            while not event.executed:
+                time.sleep(DEFAULT_SLEEP)
+            frame = event.supplements[0]
+
+        try:
+            yield frame
+        finally:
+            if close_win:
+                try:
+                    modified = frame.getController().isModified()
+                except AttributeError:
+                    modified = False
+                if modified:
+                    # Close the window and answer no when it asks if we want 
to save
+                    with 
self.execute_blocking_action(self._xUITest.executeCommandForProvider,
+                                                      args=(".uno:CloseWin", 
frame),
+                                                      close_button="no"):
+                        pass
+                else:
+                    self._xUITest.executeCommandForProvider(".uno:CloseWin", 
frame)
+                # Closing the window will happen asynchronously on the main 
thread so let’s wait
+                # until the close actually completes.
+                xToolkit = 
self._xContext.ServiceManager.createInstance('com.sun.star.awt.Toolkit')
+                xToolkit.waitUntilAllIdlesDispatched()
+
     # Calls UITest.close_doc at exit
     @contextmanager
     def create_doc_in_start_center(self, app):

Reply via email to