########################################################################## # # pgAdmin 4 - PostgreSQL Tools # # Copyright (C) 2013 - 2019, The pgAdmin Development Team # This software is released under the PostgreSQL Licence # ########################################################################## from __future__ import print_function import sys import pyperclip import random from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys from regression.python_test_utils import test_utils from regression.feature_utils.base_feature_test import BaseFeatureTest from .locators import QueryToolLocatorsCss class QueryToolJourneyTest(BaseFeatureTest): """ Tests the path through the query tool """ scenarios = [ ("Tests the path through the query tool", dict()) ] test_table_name = "" test_editable_table_name = "" def before(self): self.test_table_name = "test_table" + str(random.randint(1000, 3000)) test_utils.create_table( self.server, self.test_db, self.test_table_name) self.test_editable_table_name = "test_editable_table" + \ str(random.randint(1000, 3000)) create_sql = ''' CREATE TABLE "%s" ( pk_column NUMERIC PRIMARY KEY, normal_column NUMERIC ); ''' % self.test_editable_table_name test_utils.create_table_with_query( self.server, self.test_db, create_sql) self.page.add_server(self.server) driver_version = test_utils.get_driver_version() self.driver_version = float('.'.join(driver_version.split('.')[:2])) def runTest(self): self._navigate_to_query_tool() self._execute_query( "SELECT * FROM %s ORDER BY value " % self.test_table_name) print("Copy rows...", file=sys.stderr, end="") self._test_copies_rows() print(" OK.", file=sys.stderr) print("Copy columns...", file=sys.stderr, end="") self._test_copies_columns() print(" OK.", file=sys.stderr) print("History tab...", file=sys.stderr, end="") self._test_history_tab() print(" OK.", file=sys.stderr) # Insert data into test editable table self._insert_data_into_test_editable_table() print("History query sources and generated queries toggle...", file=sys.stderr, end="") self._test_query_sources_and_generated_queries() print(" OK.", file=sys.stderr) print("Updatable resultsets...", file=sys.stderr, end="") self._test_updatable_resultset() print(" OK.", file=sys.stderr) def _test_copies_rows(self): pyperclip.copy("old clipboard contents") self.page.driver.switch_to.default_content() self.page.driver.switch_to_frame( self.page.driver.find_element_by_tag_name("iframe")) self.page.find_by_xpath( "//*[contains(@class, 'slick-row')]/*[1]").click() self.page.find_by_xpath("//*[@id='btn-copy-row']").click() self.assertEqual('"Some-Name"\t"6"\t"some info"', pyperclip.paste()) def _test_copies_columns(self): pyperclip.copy("old clipboard contents") self.page.driver.switch_to.default_content() self.page.driver.switch_to_frame( self.page.driver.find_element_by_tag_name("iframe")) self.page.find_by_xpath( "//*[@data-test='output-column-header' and " "contains(., 'some_column')]" ).click() self.page.find_by_xpath("//*[@id='btn-copy-row']").click() self.assertTrue('"Some-Name"' in pyperclip.paste()) self.assertTrue('"Some-Other-Name"' in pyperclip.paste()) self.assertTrue('"Yet-Another-Name"' in pyperclip.paste()) def _test_history_tab(self): self.__clear_query_tool() editor_input = self.page.find_by_css_selector( QueryToolLocatorsCss.query_editor_panel) self.page.click_element(editor_input) self._execute_query("SELECT * FROM table_that_doesnt_exist") self.page.click_tab("Query History") selected_history_entry = self.page.find_by_css_selector( QueryToolLocatorsCss.query_history_selected) self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_history_entry.text) failed_history_detail_pane = self.page.find_by_css_selector( QueryToolLocatorsCss.query_history_detail) self.assertIn( "Error Message relation \"table_that_doesnt_exist\" " "does not exist", failed_history_detail_pane.text ) self.page.wait_for_element(lambda driver: driver .find_element_by_css_selector( "#query_list> .query-group>ul>li")) # get the query history rows and click the previous query row which # was executed and verify it history_rows = self.driver.find_elements_by_css_selector( "#query_list> .query-group>ul>li") history_rows[1].click() selected_history_entry = self.page.find_by_css_selector( "#query_list .selected") self.assertIn(("SELECT * FROM %s ORDER BY value" % self.test_table_name), selected_history_entry.text) # check second(invalid) query also exist in the history tab with error newly_selected_history_entry = self.page.find_by_xpath( "//*[@id='query_list']/div/ul/li[1]") self.page.click_element(newly_selected_history_entry) selected_invalid_history_entry = self.page.find_by_css_selector( "#query_list .selected .entry.error .query") self.assertIn("SELECT * FROM table_that_doesnt_exist", selected_invalid_history_entry.text) self.page.click_tab("Query Editor") self.__clear_query_tool() self.page.click_element(editor_input) self.page.fill_codemirror_area_with("SELECT * FROM hats") for _ in range(15): self.page.find_by_css_selector( QueryToolLocatorsCss.btn_execute_query).click() self.page.wait_for_query_tool_loading_indicator_to_disappear() self.page.click_tab("Query History") query_we_need_to_scroll_to = self.page.find_by_xpath( "//*[@id='query_list']/div/ul/li[17]") self.page.click_element(query_we_need_to_scroll_to) for _ in range(17): ActionChains(self.page.driver) \ .send_keys(Keys.ARROW_DOWN) \ .perform() self._assert_clickable(query_we_need_to_scroll_to) def _test_query_sources_and_generated_queries(self): self.__clear_query_history() self._test_history_query_sources() self._test_toggle_generated_queries() def _test_history_query_sources(self): self.page.click_tab("Query Editor") self._execute_sources_test_queries() self.page.click_tab("Query History") history_entries_icons = [ QueryToolLocatorsCss.commit_icon, QueryToolLocatorsCss.save_data_icon, QueryToolLocatorsCss.save_data_icon, QueryToolLocatorsCss.execute_icon, QueryToolLocatorsCss.explain_analyze_icon, QueryToolLocatorsCss.explain_icon ] history_entries_queries = [ "COMMIT;", "UPDATE public.%s SET normal_column = '10'::numeric " "WHERE pk_column = '1';" % self.test_editable_table_name, "BEGIN;", "SELECT * FROM %s" % self.test_editable_table_name, "SELECT * FROM %s" % self.test_editable_table_name, "SELECT * FROM %s" % self.test_editable_table_name ] self._check_history_queries_and_icons(history_entries_queries, history_entries_icons) def _test_toggle_generated_queries(self): xpath = '//li[contains(@class, "pgadmin-query-history-entry")]' self.assertTrue(self.page.check_if_element_exist_by_xpath(xpath)) toggle_el = self.page.find_by_xpath( '//input[@id ="generated-queries-toggle"]/..' ) toggle_el.click() self.assertFalse(self.page.check_if_element_exist_by_xpath(xpath)) toggle_el.click() self.assertTrue(self.page.check_if_element_exist_by_xpath(xpath)) def _test_updatable_resultset(self): if self.driver_version < 2.8: return self.page.click_tab("Query Editor") # Select all data (contains the primary key -> should be editable) self.__clear_query_tool() query = "SELECT pk_column, normal_column FROM %s" \ % self.test_editable_table_name self._check_query_results_editable(query, True) # Select data without primary keys -> should not be editable self.__clear_query_tool() query = "SELECT normal_column FROM %s" % self.test_editable_table_name self._check_query_results_editable(query, False) def _execute_sources_test_queries(self): self.__clear_query_tool() self._explain_query( "SELECT * FROM %s;" % self.test_editable_table_name ) self._explain_analyze_query( "SELECT * FROM %s;" % self.test_editable_table_name ) self._execute_query( "SELECT * FROM %s;" % self.test_editable_table_name ) # Turn off autocommit query_options = self.page.find_by_css_selector( QueryToolLocatorsCss.btn_query_dropdown) query_options.click() self.page.find_by_css_selector( QueryToolLocatorsCss.btn_auto_commit).click() query_options.click() # Click again to close dropdown self._update_numeric_cell(2, 10) self._commit_transaction() # Turn on autocommit query_options = self.page.find_by_css_selector( QueryToolLocatorsCss.btn_query_dropdown) query_options.click() self.page.find_by_css_selector( QueryToolLocatorsCss.btn_auto_commit).click() query_options.click() # Click again to close dropdown def _check_history_queries_and_icons(self, history_queries, history_icons): # Select first query history entry self.page.find_by_xpath("//*[@id='query_list']/div/ul/li[1]").click() for icon, query in zip(history_icons, history_queries): # Check query query_history_selected_item = self.page.find_by_css_selector( QueryToolLocatorsCss.query_history_selected ) self.assertIn(query, query_history_selected_item.text) # Check source icon query_history_selected_icon = self.page.find_by_css_selector( QueryToolLocatorsCss.query_history_selected_icon) icon_classes = query_history_selected_icon.get_attribute('class') icon_classes = icon_classes.split(" ") self.assertTrue(icon in icon_classes) # Move to next entry ActionChains(self.page.driver) \ .send_keys(Keys.ARROW_DOWN) \ .perform() def _update_numeric_cell(self, cell_index, value): """ Updates a numeric cell in the first row of the resultset """ xpath = '//div[contains(@class, "slick-row") and ' \ 'contains(@style, "top:0px")]' xpath += '/div[contains(@class, "slick-cell") and ' \ 'contains(@class, "r' + str(cell_index) + '")]' cell_el = self.page.find_by_xpath(xpath) ActionChains(self.driver).double_click(cell_el).perform() ActionChains(self.driver).send_keys(value). \ send_keys(Keys.ENTER).perform() self.page.find_by_css_selector( QueryToolLocatorsCss.btn_save_data).click() def _insert_data_into_test_editable_table(self): self.page.click_tab("Query Editor") self.__clear_query_tool() self._execute_query( "INSERT INTO %s VALUES (1, 1), (2, 2);" % self.test_editable_table_name ) def __clear_query_tool(self): self.page.click_element( self.page.find_by_xpath("//*[@id='btn-clear-dropdown']") ) ActionChains(self.driver)\ .move_to_element(self.page.find_by_xpath("//*[@id='btn-clear']"))\ .perform() self.page.click_element( self.page.find_by_xpath("//*[@id='btn-clear']") ) self.page.click_modal('Yes') def __clear_query_history(self): self.page.click_element( self.page.find_by_xpath("//*[@id='btn-clear-dropdown']") ) ActionChains(self.driver)\ .move_to_element( self.page.find_by_xpath( "//*[@id='btn-clear-history']")).perform() self.page.click_element( self.page.find_by_xpath("//*[@id='btn-clear-history']") ) self.page.click_modal('Yes') def _navigate_to_query_tool(self): self.page.toggle_open_tree_item(self.server['name']) self.page.toggle_open_tree_item('Databases') self.page.toggle_open_tree_item(self.test_db) self.page.open_query_tool() self.page.wait_for_spinner_to_disappear() def _execute_query(self, query): self.page.fill_codemirror_area_with(query) self.page.find_by_css_selector( QueryToolLocatorsCss.btn_execute_query).click() def _explain_query(self, query): self.page.fill_codemirror_area_with(query) self.page.find_by_css_selector( QueryToolLocatorsCss.btn_explain).click() def _explain_analyze_query(self, query): self.page.fill_codemirror_area_with(query) self.page.find_by_css_selector( QueryToolLocatorsCss.btn_explain_analyze).click() def _commit_transaction(self): self.page.find_by_css_selector( QueryToolLocatorsCss.btn_commit).click() def _assert_clickable(self, element): self.page.click_element(element) def _check_query_results_editable(self, query, should_be_editable): self._execute_query(query) self.page.wait_for_spinner_to_disappear() # Check if the first cell in the first row is editable is_editable = self._check_cell_editable(1) self.assertEqual(is_editable, should_be_editable) def _check_cell_editable(self, cell_index): """ Checks if a cell in the first row of the resultset is editable """ xpath = '//div[contains(@class, "slick-row") and ' \ 'contains(@style, "top:0px")]' xpath += '/div[contains(@class, "slick-cell") and ' \ 'contains(@class, "r' + str(cell_index) + '")]' cell_el = self.page.find_by_xpath(xpath) # Get existing value cell_value = int(cell_el.text) new_value = cell_value + 1 # Try to update value ActionChains(self.driver).double_click(cell_el).perform() ActionChains(self.driver).send_keys(new_value). \ send_keys(Keys.ENTER).perform() # Check if the value was updated return int(cell_el.text) == new_value def after(self): self.page.close_query_tool() self.page.remove_server(self.server)