1# Copyright 2008-2011 Nokia Networks
2# Copyright 2011-2016 Ryan Tomac, Ed Manlove and contributors
3# Copyright 2016-     Robot Framework Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#    http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from SeleniumLibrary.base import LibraryComponent, keyword
18
19
20class TableElementKeywords(LibraryComponent):
21
22    @keyword
23    def get_table_cell(self, locator, row, column, loglevel='TRACE'):
24        """Returns contents of table cell.
25
26        The table is located using the ``locator`` argument and its cell
27        found using ``row`` and ``column``. See the `Locating elements`
28        section for details about the locator syntax.
29
30        Both row and column indexes start from 1, and header and footer
31        rows are included in the count. It is possible to refer to rows
32        and columns from the end by using negative indexes so that -1
33        is the last row/column, -2 is the second last, and so on.
34
35        All ``<th>`` and ``<td>`` elements anywhere in the table are
36        considered to be cells.
37
38        See `Page Should Contain` for explanation about the ``loglevel``
39        argument.
40        """
41        row = int(row)
42        column = int(column)
43        if row == 0 or column == 0:
44            raise ValueError('Both row and column must be non-zero, '
45                             'got row %d and column %d.' % (row, column))
46        try:
47            cell = self._get_cell(locator, row, column)
48        except AssertionError:
49            self.log_source(loglevel)
50            raise
51        return cell.text
52
53    def _get_cell(self, locator, row, column):
54        rows = self._get_rows(locator, row)
55        if len(rows) < abs(row):
56            raise AssertionError("Table '%s' should have had at least %d "
57                                 "rows but had only %d."
58                                 % (locator, abs(row), len(rows)))
59        index = row - 1 if row > 0 else row
60        cells = rows[index].find_elements_by_xpath('./th|./td')
61        if len(cells) < abs(column):
62            raise AssertionError("Table '%s' row %d should have had at "
63                                 "least %d columns but had only %d."
64                                 % (locator, row, abs(column), len(cells)))
65        index = column - 1 if column > 0 else column
66        return cells[index]
67
68    def _get_rows(self, locator, count):
69        # Get rows in same order as browsers render them.
70        table = self.find_element(locator, tag='table')
71        rows = table.find_elements_by_xpath("./thead/tr")
72        if count < 0 or len(rows) < count:
73            rows.extend(table.find_elements_by_xpath("./tbody/tr"))
74        if count < 0 or len(rows) < count:
75            rows.extend(table.find_elements_by_xpath("./tfoot/tr"))
76        return rows
77
78    @keyword
79    def table_cell_should_contain(self, locator, row, column, expected, loglevel='TRACE'):
80        """Verifies table cell contains text ``expected``.
81
82        See `Get Table Cell` that this keyword uses internally for
83        explanation about accepted arguments.
84        """
85        content = self.get_table_cell(locator, row, column, loglevel)
86        if expected not in content:
87            self.ctx.log_source(loglevel)
88            raise AssertionError("Table '%s' cell on row %s and column %s "
89                                 "should have contained text '%s' but it had "
90                                 "'%s'."
91                                 % (locator, row, column, expected, content))
92        self.info("Table cell contains '%s'." % content)
93
94    @keyword
95    def table_column_should_contain(self, locator, column, expected, loglevel='TRACE'):
96        """Verifies table column contains text ``expected``.
97
98        The table is located using the ``locator`` argument and its column
99        found using ``column``. See the `Locating elements` section for
100        details about the locator syntax.
101
102        Column indexes start from 1. It is possible to refer to columns
103        from the end by using negative indexes so that -1 is the last column,
104        -2 is the second last, and so on.
105
106        If a table contains cells that span multiple columns, those merged
107        cells count as a single column.
108
109        See `Page Should Contain Element` for explanation about the
110        ``loglevel`` argument.
111        """
112        element = self._find_by_column(locator, column, expected)
113        if element is None:
114            self.ctx.log_source(loglevel)
115            raise AssertionError("Table '%s' column %s did not contain text "
116                                 "'%s'." % (locator, column, expected))
117
118    @keyword
119    def table_footer_should_contain(self, locator, expected, loglevel='TRACE'):
120        """Verifies table footer contains text ``expected``.
121
122        Any ``<td>`` element inside ``<tfoot>`` element is considered to
123        be part of the footer.
124
125        The table is located using the ``locator`` argument. See the
126        `Locating elements` section for details about the locator syntax.
127
128        See `Page Should Contain Element` for explanation about the
129        ``loglevel`` argument.
130        """
131        element = self._find_by_footer(locator, expected)
132        if element is None:
133            self.ctx.log_source(loglevel)
134            raise AssertionError("Table '%s' footer did not contain text "
135                                 "'%s'." % (locator, expected))
136
137    @keyword
138    def table_header_should_contain(self, locator, expected, loglevel='TRACE'):
139        """Verifies table header contains text ``expected``.
140
141        Any ``<th>`` element anywhere in the table is considered to be
142        part of the header.
143
144        The table is located using the ``locator`` argument. See the
145        `Locating elements` section for details about the locator syntax.
146
147        See `Page Should Contain Element` for explanation about the
148        ``loglevel`` argument.
149        """
150        element = self._find_by_header(locator, expected)
151        if element is None:
152            self.ctx.log_source(loglevel)
153            raise AssertionError("Table '%s' header did not contain text "
154                                 "'%s'." % (locator, expected))
155
156    @keyword
157    def table_row_should_contain(self, locator, row, expected, loglevel='TRACE'):
158        """Verifies that table row contains text ``expected``.
159
160        The table is located using the ``locator`` argument and its column
161        found using ``column``. See the `Locating elements` section for
162        details about the locator syntax.
163
164        Row indexes start from 1. It is possible to refer to rows
165        from the end by using negative indexes so that -1 is the last row,
166        -2 is the second last, and so on.
167
168        If a table contains cells that span multiple rows, a match
169        only occurs for the uppermost row of those merged cells.
170
171        See `Page Should Contain Element` for explanation about the
172        ``loglevel`` argument.
173        """
174        element = self._find_by_row(locator, row, expected)
175        if element is None:
176            self.ctx.log_source(loglevel)
177            raise AssertionError("Table '%s' row %s did not contain text "
178                                 "'%s'." % (locator, row, expected))
179
180    @keyword
181    def table_should_contain(self, locator, expected, loglevel='TRACE'):
182        """Verifies table contains text ``expected``.
183
184        The table is located using the ``locator`` argument. See the
185        `Locating elements` section for details about the locator syntax.
186
187        See `Page Should Contain Element` for explanation about the
188        ``loglevel`` argument.
189        """
190        element = self._find_by_content(locator, expected)
191        if element is None:
192            self.ctx.log_source(loglevel)
193            raise AssertionError("Table '%s' did not contain text '%s'."
194                                 % (locator, expected))
195
196    def _find_by_content(self, table_locator, content):
197        return self._find(table_locator, '//*', content)
198
199    def _find_by_header(self, table_locator, content):
200        return self._find(table_locator, '//th', content)
201
202    def _find_by_footer(self, table_locator, content):
203        return self._find(table_locator, '//tfoot//td', content)
204
205    def _find_by_row(self, table_locator, row, content):
206        position = self._index_to_position(row)
207        locator = '//tr[{}]'.format(position)
208        return self._find(table_locator, locator, content)
209
210    def _find_by_column(self, table_locator, col, content):
211        position = self._index_to_position(col)
212        locator = '//tr//*[self::td or self::th][{}]'.format(position)
213        return self._find(table_locator, locator, content)
214
215    def _index_to_position(self, index):
216        index = int(index)
217        if index == 0:
218            raise ValueError('Row and column indexes must be non-zero.')
219        if index > 0:
220            return str(index)
221        if index == -1:
222            return 'position()=last()'
223        return 'position()=last()-{}'.format(abs(index) - 1)
224
225    def _find(self, table_locator, locator, content):
226        table = self.find_element(table_locator)
227        elements = self.find_elements(locator, parent=table)
228        for element in elements:
229            if content is None:
230                return element
231            if element.text and content in element.text:
232                return element
233        return None
234