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):
