From 97d9f40bb14c32fb40464ddd6152faa2f4a6280d Mon Sep 17 00:00:00 2001 From: Yash Ajgaonkar Date: Mon, 26 Aug 2024 17:45:07 +0530 Subject: [PATCH] added support for boxplot --- README.md | 1 + src/widgetastic_patternfly5/__init__.py | 2 + .../charts/boxplot_chart.py | 105 ++++++++++++++++++ testing/charts/test_boxplot_chart.py | 44 ++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/widgetastic_patternfly5/charts/boxplot_chart.py create mode 100644 testing/charts/test_boxplot_chart.py diff --git a/README.md b/README.md index e1ef261..abad45a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ itteration of [widgetastic.patternfly4](https://github.com/RedHatQE/widgetastic. ### Charts: - [bullet-chart](https://www.patternfly.org/charts/bullet-chart) +- [boxplot-chart](https://patternfly-react-main.surge.sh/charts/box-plot-chart) - [donut-chart](https://www.patternfly.org/charts/donut-chart) - [legends](https://www.patternfly.org/charts/legends) - [line-chart](https://www.patternfly.org/charts/line-chart) diff --git a/src/widgetastic_patternfly5/__init__.py b/src/widgetastic_patternfly5/__init__.py index e73ebc7..350e259 100644 --- a/src/widgetastic_patternfly5/__init__.py +++ b/src/widgetastic_patternfly5/__init__.py @@ -1,3 +1,4 @@ +from .charts.boxplot_chart import BoxPlotChart from .charts.bullet_chart import BulletChart from .charts.donut_chart import DonutChart from .charts.legend import DataPoint @@ -72,6 +73,7 @@ __all__ = [ "Alert", "BreadCrumb", + "BoxPlotChart", "BulletChart", "Button", "CalendarMonth", diff --git a/src/widgetastic_patternfly5/charts/boxplot_chart.py b/src/widgetastic_patternfly5/charts/boxplot_chart.py new file mode 100644 index 0000000..f782f7c --- /dev/null +++ b/src/widgetastic_patternfly5/charts/boxplot_chart.py @@ -0,0 +1,105 @@ +from widgetastic.widget import ParametrizedLocator +from widgetastic.widget import View +from widgetastic.xpath import quote + +from .legend import Legend + + +class BoxPlotChart(View): + """Represents the Patternfly Line Chart. + + https://patternfly-react-main.surge.sh/charts/box-plot-chart#embedded-legend + + Args: + id: If you want to look the input up by id, use this parameter, pass the id. + locator: If you have specific locator else it will take pf-chart. + """ + + ROOT = ParametrizedLocator("{@locator}") + + X_AXIS_LABELS = "(.//*[name()='g' and *[name()='line'] and *[name()='g']])[1]//*[name()='text']" + Y_AXIS_LABELS = "(.//*[name()='g' and *[name()='line'] and *[name()='g']])[2]//*[name()='text']" + + TOOLTIP = ( + ".//*[name()='g' and .//*[name()='g' and " + ".//*[name()='text' and contains(@id, 'legend-labels')]]]" + ) + + TOOLTIP_X_AXIS_LABLE = ".//*[name()='text']" + TOOLTIP_LABLES = ".//*[name()='g']/*[name()='text' and not(contains(@id, 'legend-label'))]" + TOOLTIP_VALUES = ".//*[name()='g']/*[name()='text' and contains(@id, 'legend-label')]" + + _legends = View.nested(Legend) + + def __init__(self, parent=None, id=None, locator=None, logger=None): + View.__init__(self, parent=parent, logger=logger) + + assert id or locator, "Provide id or locator." + + if id: + self.locator = ".//div[@id={}]".format(quote(id)) + else: + self.locator = locator + + @property + def legends(self): + """Return object of Legends""" + return [leg for leg in self._legends] + + @property + def legend_names(self): + """Return all legend names.""" + return [leg.label for leg in self.legends] + + def get_legend(self, label): + """Get specific Legend object. + + Args: + label: Name of legend label. + """ + try: + return next(leg for leg in self.legends if leg.label == label) + except StopIteration: + return None + + @property + def _x_axis_labels_map(self): + return {self.browser.text(el): el for el in self.browser.elements(self.X_AXIS_LABELS)} + + @property + def labels_x_axis(self): + """Return X-Axis labels.""" + return list(self._x_axis_labels_map.keys()) + + def read(self, offset=(0, -100)): + """Read chart data. + + Note: This method has some limitations as we are reading the tooltip for x-axis labels + with some offset. So only applicable for the chart which shows all Legend data in a single + tooltip for the respective x-axis label. + + Args: + offset: offset to move the cursor from the x-axis label so that the tooltip can appear. + """ + _data = {} + + for lab_el in self._x_axis_labels_map.values(): + self.browser.move_to_element(lab_el) + self.browser.click(lab_el) + self.browser.move_by_offset(*offset) + tooltip_el = self.browser.wait_for_element(self.TOOLTIP) + + x_axis_label = self.browser.text(self.TOOLTIP_X_AXIS_LABLE, parent=tooltip_el) + label_data = {} + + for label_el, value_el in zip( + self.browser.elements(self.TOOLTIP_LABLES, parent=tooltip_el), + self.browser.elements(self.TOOLTIP_VALUES, parent=tooltip_el), + ): + label_data[self.browser.text(label_el)] = self.browser.text(value_el) + + _data[x_axis_label] = label_data + + # Just move cursor to avoid mismatch of legend and tooltip text. + self.root_browser.move_to_element(".//body") + return _data diff --git a/testing/charts/test_boxplot_chart.py b/testing/charts/test_boxplot_chart.py new file mode 100644 index 0000000..5e42a12 --- /dev/null +++ b/testing/charts/test_boxplot_chart.py @@ -0,0 +1,44 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5 import BoxPlotChart + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/charts/box-plot-chart" + +TEST_DATA = { + "2015": {"Cats": "q1: 1.75, q3: 3.5", "Limit": "12"}, + "2016": {"Cats": "q1: 2.75, q3: 8.5", "Limit": "12"}, + "2017": {"Cats": "q1: 4.25, q3: 6.5", "Limit": "12"}, + "2018": {"Cats": "q1: 1.75, q3: 4.5", "Limit": "12"}, +} + + +@pytest.fixture +def view(browser): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-box-plot-chart-embedded-legend']" + chart = BoxPlotChart(locator=".//div[@class='pf-v5-c-chart']") + + return TestView(browser) + + +def test_line_chart(view): + legend_names = view.chart.legend_names + + # check chart is displayed + assert view.chart.is_displayed + + # validate chart data + assert view.chart.read() == TEST_DATA + + expected_legend_names = list(TEST_DATA.values())[0].keys() + + # validate legend names + assert set(legend_names) == set(expected_legend_names) + + # validate x-axis keys + assert view.chart.labels_x_axis == list(TEST_DATA.keys()) + + # Validate Legends + cats_legend = view.chart.get_legend("Cats") + assert cats_legend.label == "Cats"