Source code for pytest_splunk_addon_ui_smartx.components.table

#
# Copyright 2023 Splunk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import copy
import re
import time
from contextlib import contextmanager

from selenium import webdriver
from selenium.common import exceptions
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

from .base_component import BaseComponent, Selector
from .dropdown import Dropdown


[docs]class Table(BaseComponent): """ Component: Table Base class of Input & Configuration table """ def __init__(self, browser, container, mapping=dict(), wait_for_seconds=10): """ :param browser: The selenium webdriver :param container: Container in which the table is located. Of type dictionary: {"by":..., "select":...} :param mapping= If the table headers are different from it's html-label, provide the mapping as dictionary. For ex, {"Status": "disabled"} """ super().__init__(browser, container) self.header_mapping = mapping self.browser = browser self.elements.update( { "rows": Selector( select=container.select + ' tbody[data-test="body"] tr[data-test="row"]' ), "header": Selector( select=container.select + ' th[data-test="head-cell"]' ), "app_listings": Selector( select=container.select + ' tbody[data-test="body"]' ), "action_values": Selector( select=container.select + ' [data-test="toggle"]' ), "col": Selector( select=container.select + ' [data-test="cell"][data-column="{column}"]' ), "col-number": Selector( select=container.select + " td:nth-child({col_number})" ), "edit": Selector(select=".editBtn"), "clone": Selector(select=".cloneBtn"), "delete": Selector(select=".deleteBtn"), "delete_prompt": Selector(select=".deletePrompt"), # [data-test="body"] "delete_btn": Selector(select='[data-test="button"][label="Delete"]'), "delete_cancel": Selector( select='[data-test="button"][label="Cancel"]' ), "delete_close": Selector(select='[data-test="close"]'), "delete_loading": Selector(select='button[data-test="wait-spinner"]'), "waitspinner": Selector( select=container.select + ' [data-test="wait-spinner"]' ), "count": Selector(select=container.select + " .inputNumber"), "filter": Selector(select=container.select + ' [data-test="textbox"]'), "filter_clear": Selector( select=container.select + ' [data-test="clear"]' ), "more_info": Selector( select=container.select + ' [data-test="expand"]' ), "more_info_row": Selector( select=container.select + ' [data-expansion-row="true"]' ), "more_info_key": Selector(select='[data-test="term"]'), "more_info_value": Selector(select='[data-test="description"]'), "switch_to_page": Selector( select=container.select + " button[data-test-page]" ), "alert_sign": Selector( select=container.select + ' [data-test="alert-icon"]' ), "status_cell": Selector(select='[data-test="status"]'), } ) self.wait_for_seconds = wait_for_seconds
[docs] def get_count_title(self): """ Get the count mentioned in the table title :return: Str The count of the table title """ return self.get_clear_text(self.count)
[docs] def get_row_count(self): """ Count the number of rows in the page. :return: Int The count of the table rows """ return len(list(self._get_rows()))
[docs] def get_headers(self): """ Get list of headers from the table :return: Generator for Str list The headers in the table """ headers = [] GET_PARENT_ELEMENT = ( "var parent = arguments[0].firstChild.firstChild;if(parent.hasChildNodes()){var r='';var C=parent.childNodes;" "for(var n=0;n<C.length;n++){if(C[n].nodeType==Node.TEXT_NODE){r+=' '+C[n].nodeValue}}" "return r.trim()}else{return parent.innerText}" ) for each in self.get_elements("header"): parent_text = self.browser.execute_script(GET_PARENT_ELEMENT, each) headers.append(parent_text) return headers
[docs] def get_sort_order(self): """ Get the column-header which is sorted rn. Warning: It depends on the class of the headers and due to it, the returned result might give wrong answer. :returns: a dictionary with the "header" & "ascending" order """ for each_header in self.get_elements("header"): if each_header.get_attribute("data-test-sort-dir") == "asc": return {"header": self.get_clear_text(each_header), "ascending": True} elif each_header.get_attribute("data-test-sort-dir") == "desc": return {"header": self.get_clear_text(each_header), "ascending": False}
[docs] def sort_column(self, column, ascending=True): """ Sort a column in ascending or descending order :param column: The header of the column which should be sorted :param ascending: True if the column should be sorted in ascending order, False otherwise """ for each_header in self.get_elements("header"): if self.get_clear_text(each_header).lower() == column.lower(): if ( "asc" in each_header.get_attribute("data-test-sort-dir") and ascending ): # If the column is already in ascending order, do nothing return elif ( "asc" in each_header.get_attribute("data-test-sort-dir") and not ascending ): # If the column is in ascending order order and we want to have descending order, click on the column-header once each_header.click() self._wait_for_loadspinner() return elif ( "desc" in each_header.get_attribute("data-test-sort-dir") and not ascending ): # If the column is already in descending order, do nothing return elif ( "desc" in each_header.get_attribute("data-test-sort-dir") and ascending ): # If the column is in descending order order and we want to have ascending order, click on the column-header once each_header.click() self._wait_for_loadspinner() return else: # The column was not sorted before if ascending: # Click to sort ascending order each_header.click() self._wait_for_loadspinner() return else: # Click 2 times to sort in descending order # Ascending each_header.click() self._wait_for_loadspinner() # Decending # The existing element changes (class will be changed), hence, it can not be referenced again. # So we need to get the headers again and do the same process. self.sort_column(column, ascending=False) return
def _wait_for_loadspinner(self): """ There exist a loadspinner when sorting/filter has been applied. This method will wait until the spinner is dissapeared """ try: self.wait_for("waitspinner", timeout=5) self.wait_until("waitspinner") except: print("Waitspinner did not appear")
[docs] def wait_for_rows_to_appear(self, row_count=1): """ Wait for the table to load row_count rows :param row_count: number of row_count to wait for. """ def _wait_for_rows_to_appear(driver): return self.get_row_count() >= row_count self.wait_for( _wait_for_rows_to_appear, msg="Expected rows : {} to be greater or equal to {}".format( row_count, self.get_row_count() ), )
[docs] def wait_for_column_to_appear(self, column_name): """ Wait for the table to load the column with the given column name. :param column_name: Name of the column to wait for. """ def _wait_for_column_to_appear(driver): return column_name in self.get_headers() self.wait_for( _wait_for_column_to_appear, msg="Column {} not found in the table".format(column_name), )
[docs] def get_table(self): """ Get whole table in dictionary form. The row_name will will be the key and all header:values will be it's value. {row_1 : {header_1: value_1, . . .}, . . .} :return: dict The data within the table """ table = dict() headers = list(self.get_headers()) for each_row in self._get_rows(): row_name = self._get_column_value(each_row, "name") table[row_name] = dict() for each_col in headers: each_col = each_col.lower() if each_col == "actions": table[row_name][each_col] = "" if self.edit != None: table[row_name][each_col] = "Edit" if self.clone != None: table[row_name][each_col] += " | Clone" if self.delete != None: table[row_name][each_col] += " | Delete" continue if each_col == "status": table[row_name][each_col] = each_row.find_element_by_css_selector( '[data-test="status"]' ).text continue if each_col: table[row_name][each_col] = self._get_column_value( each_row, each_col ) return table
[docs] def get_cell_value(self, name, column): """ Get a specific cell value. :param name: row_name of the table :param column: column header of the table :return: str The value within the cell that we are looking for """ _row = self._get_row(name) if column.lower() == "status": return _row.find_element_by_css_selector('[data-test="status"]').text return self._get_column_value(_row, column)
[docs] def get_column_values(self, column): """ Get list of values of column :param column: column header of the table :return: List The values within the certain column """ value_list = [] for each_row in self._get_rows(): value_list.append(self._get_column_value(each_row, column)) return value_list
[docs] def get_list_of_actions(self, name): """ Get list of possible actions for a specific row :param name: The name of the row :return: Generator List The list of actions available within a certain row of the table """ value_list = [] _row = self._get_row(name) if _row.find_element(*list(self.elements["edit"]._asdict().values())) != None: value_list.append("Edit") if _row.find_element(*list(self.elements["clone"]._asdict().values())) != None: value_list.append("Clone") if _row.find_element(*list(self.elements["delete"]._asdict().values())) != None: value_list.append("Delete") return value_list
[docs] def edit_row(self, name): """ Edit the specified row. It will open the edit form(entity). The opened entity should be interacted with instance of entity-class only. :param name: row_name of the table """ _row = self._get_row(name) _row.find_element(*list(self.elements["edit"]._asdict().values())).click()
[docs] def clone_row(self, name): """ Clone the specified row. It will open the edit form(entity). The opened entity should be interacted with instance of entity-class only. :param name: row_name of the table """ _row = self._get_row(name) _row.find_element(*list(self.elements["clone"]._asdict().values())).click()
[docs] def delete_row(self, name, cancel=False, close=False, prompt_msg=False): """ Delete the specified row. Clicking on delete will open a pop-up. Delete the row if neither of (cancel, close) specified. :param name: row_name of the table :param cancel: if provided, after the popup is opened, click on cancel button and Do Not delete the row :param close: if provided, after the popup is opened, click on close button and Do Not delete the row :return: Bool Returns true if successful or returns the string of the delete prompt if looking for prompt message """ # Click on action with self.wait_stale(): _row = self._get_row(name) _row.find_element(*list(self.elements["delete"]._asdict().values())).click() self.wait_for("delete_prompt") if cancel: self.delete_cancel.click() self.wait_until("delete_cancel") return True elif close: self.delete_close.click() self.wait_until("delete_close") return True elif prompt_msg: self.wait_for_text("delete_prompt") return self.get_clear_text(self.delete_prompt) else: self.delete_btn.click() self.wait_until("waitspinner")
[docs] def set_filter(self, filter_query): """ Provide a string in table filter. :param filter_query: query of the filter :returns: resultant list of filtered row_names """ with self.wait_stale(): self.filter.clear() self.filter.send_keys(filter_query) self._wait_for_loadspinner() return self.get_column_values("name")
@contextmanager def wait_stale(self): rows = list(self._get_rows()) col = copy.deepcopy(self.elements["col"]) col = col._replace(select=col.select.format(column="name")) col_element = self._get_element(col.by, col.select) yield if len(rows) > 0 and self.wait_to_be_stale(rows[0]): self.wait_to_be_stale(col_element)
[docs] def clean_filter(self): """ Clean the filter textbox """ self.filter.clear() self._wait_for_loadspinner()
def _get_column_value(self, row, column): """ Get the column from a specific row provided. :param row: the webElement of the row :param column: the header name of the column :return: The list of column values from a specific column and row """ find_by_col_number = False if column.lower().replace(" ", "_") in self.header_mapping: column = self.header_mapping[column.lower().replace(" ", "_")] find_by_col_number = isinstance(column, int) else: column = column.lower().replace(" ", "_") if not find_by_col_number: col = copy.deepcopy(self.elements["col"]) col = col._replace(select=col.select.format(column=column)) self.wait_for("app_listings") return self.get_clear_text(row.find_element(*list(col._asdict().values()))) else: # Int value col = copy.deepcopy(self.elements["col-number"]) col = col._replace(select=col.select.format(col_number=column)) self.wait_for("app_listings") return self.get_clear_text(row.find_element(*list(col._asdict().values()))) def _get_rows(self): """ Get list of rows :return: The list of rows within the table """ yield from self.get_elements("rows") def _get_row(self, name): """ Get the specified row. :param name: row name :return: element Gets the row specified within the table, or raises a warning if not found """ for each_row in self._get_rows(): if self._get_column_value(each_row, "name") == name: return each_row else: raise ValueError("{} row not found in table".format(name))
[docs] def get_action_values(self, name): """ Get the specified rows action values :param name: row name :return: List Gets the action values of the row specified within the table """ _row = self._get_row(name) return [ self.get_clear_text(each) for each in self.get_elements("action_values") ]
[docs] def get_count_number(self): """ Returns the count from the title of the table. :return: Int The title count of the table. """ row_count = self.get_count_title() return int(re.search(r"\d+", row_count).group())
[docs] def get_more_info(self, name, cancel=True): """ Returns the text from the more info field within a tables row :param name: Str row name :param cancel: Bool Whether or not to click cancel after getting the info :return: Dict The information found when opening the info table on a row in the table """ _row = self._get_row(name) _row.find_element(*list(self.elements["more_info"]._asdict().values())).click() keys = self.more_info_row.find_elements( *list(self.elements["more_info_key"]._asdict().values()) ) values = self.more_info_row.find_elements( *list(self.elements["more_info_value"]._asdict().values()) ) more_info = { self.get_clear_text(key): self.get_clear_text(value) for key, value in zip(keys, values) } if cancel: _row = self._get_row(name) _row.find_element( *list(self.elements["more_info"]._asdict().values()) ).click() return more_info
[docs] def switch_to_page(self, value): """ Switches the table to specified page :param value: Int The page to switch the table to :return: Bool whether or not switching to the page was successful """ for each in self.get_elements("switch_to_page"): if self.get_clear_text(each).lower() not in [ "prev", "next", ] and self.get_clear_text(each) == str(value): each.click() return True else: raise ValueError("{} not found".format(value))
[docs] def switch_to_prev(self): """ Switches the table's page back by 1 :return: Bool whether or not switching to the previous page was successful """ for page_prev in self.get_elements("switch_to_page"): if self.get_clear_text(page_prev).lower() == "prev": page_prev.click() return True else: raise ValueError("{} not found".format(page_prev))
[docs] def switch_to_next(self): """ Switches the table's page forward by 1 :return: Bool whether or not switching to the next page was successful """ for page_next in self.get_elements("switch_to_page"): if self.get_clear_text(page_next).lower() == "next": page_next.click() return True else: raise ValueError("{} not found".format(page_next))
[docs] def check_alert_sign(self, row_name, column_name="account"): """ This function check account warning present in the table while account is not configured in input :param row_name: the name of the row :param column_name: the header name of the column """ column_selector = column_name.lower().replace(" ", "_") column_selector = self.header_mapping.get(column_selector, column_name) col = copy.deepcopy(self.elements["alert_sign"]) col = col._replace(select=col.select.format(column=column_selector)) _row = self._get_row(row_name) try: _row.find_element(*list(col._asdict().values())) return True except exceptions.NoSuchElementException: return False