getLabelText() {
+ return getLabel().map(WebElement::getText);
+ }
+
+ /**
+ * The same as {@link #getLabelText()}.
+ *
+ * @return Text of the checkbox label or {@code null} if no label has been found.
+ */
+ public String getText() {
+ return getLabelText().orElse("");
+ }
+
+ /** Selects checkbox if it is not already selected. */
+ public void select() {
+ if (!isSelected()) {
+ getWrappedElement().click();
+ }
+ }
+
+ /** Deselects checkbox if it is not already deselected. */
+ public void deselect() {
+ if (isSelected()) {
+ getWrappedElement().click();
+ }
+ }
+
+ /** Selects checkbox if passed value is {@code true} and deselects otherwise. */
+ public void set(boolean value) {
+ if (value) {
+ select();
+ } else {
+ deselect();
+ }
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/FileInput.java b/src/main/java/com/frameworkium/core/htmlelements/element/FileInput.java
new file mode 100755
index 00000000..9135d873
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/FileInput.java
@@ -0,0 +1,101 @@
+package com.frameworkium.core.htmlelements.element;
+
+import com.frameworkium.core.common.properties.Property;
+import com.frameworkium.core.ui.UITestLifecycle;
+import org.openqa.selenium.*;
+import org.openqa.selenium.remote.*;
+import org.openqa.selenium.support.events.EventFiringWebDriver;
+
+import java.io.File;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.frameworkium.core.htmlelements.utils.HtmlElementUtils.*;
+
+/** Represents web page file upload element. */
+public class FileInput extends TypifiedElement {
+
+ /**
+ * Specifies wrapped {@link WebElement}.
+ *
+ * @param wrappedElement {@code WebElement} to wrap.
+ */
+ public FileInput(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Sets a file to be uploaded.
+ *
+ * File is searched in the following way: if a resource with a specified name exists in classpath,
+ * then this resource will be used, otherwise file will be searched on file system.
+ *
+ * @param fileName Name of a file or a resource to be uploaded.
+ */
+ public void setFileToUpload(final String fileName) {
+ // Set local file detector in case of remote driver usage
+ setLocalFileDetectorIfRequired();
+
+ String filePath = getFilePath(fileName);
+ sendKeys(filePath);
+ }
+
+ /**
+ * Sets multiple files to be uploaded.
+ *
+ * Files are searched in the following way:
+ * if a resource with a specified name exists in classpath,
+ * then this resource will be used, otherwise file will be searched on file system.
+ *
+ * @param fileNames a list of file Names to be uploaded.
+ */
+ public void setFilesToUpload(List fileNames) {
+ // Set local file detector in case of remote driver usage
+ setLocalFileDetectorIfRequired();
+
+ String filePaths = fileNames.stream()
+ .map(this::getFilePath)
+ .collect(Collectors.joining("\n"));
+ sendKeys(filePaths);
+ }
+
+ private void setLocalFileDetectorIfRequired() {
+ if (Property.GRID_URL.isSpecified()) {
+ WebDriver webDriver = UITestLifecycle.get().getWebDriver();
+ EventFiringWebDriver efDriver = (EventFiringWebDriver) webDriver;
+ RemoteWebDriver remoteDriver = (RemoteWebDriver) efDriver.getWrappedDriver();
+ remoteDriver.setFileDetector(new LocalFileDetector());
+ }
+ }
+
+ /**
+ * Submits selected file by simply submitting the whole form, which contains this file input.
+ */
+ public void submit() {
+ getWrappedElement().submit();
+ }
+
+ private WebElement getNotProxiedInputElement() {
+ return getWrappedElement().findElement(By.xpath("."));
+ }
+
+ private void setLocalFileDetector(RemoteWebElement element) {
+ element.setFileDetector(new LocalFileDetector());
+ }
+
+ private String getFilePath(final String fileName) {
+ if (existsInClasspath(fileName)) {
+ return getPathForResource(fileName);
+ }
+ return getPathForSystemFile(fileName);
+ }
+
+ private String getPathForResource(final String fileName) {
+ return getResourceFromClasspath(fileName).getPath();
+ }
+
+ private String getPathForSystemFile(final String fileName) {
+ File file = new File(fileName);
+ return file.getPath();
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Form.java b/src/main/java/com/frameworkium/core/htmlelements/element/Form.java
new file mode 100755
index 00000000..75318a08
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Form.java
@@ -0,0 +1,115 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.*;
+
+import static java.util.Objects.isNull;
+
+/**
+ * Represents web page form tag.
+ * Provides handy way of filling form with data and submitting it.
+ */
+public class Form extends TypifiedElement {
+
+ private static final String CHECKBOX_FIELD = "checkbox";
+ private static final String RADIO_FIELD = "radio";
+ private static final String SELECT_FIELD = "select";
+ private static final String INPUT_FIELD = "input";
+ private static final String FILE_FIELD = "file";
+
+ /**
+ * Specifies {@link WebElement} representing form tag.
+ *
+ * @param wrappedElement {@code WebElement} to wrap.
+ */
+ public Form(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Fills form with data contained in passed map.
+ * For each map entry if an input with a name coincident with entry key exists
+ * it is filled with string representation of entry value.
+ * If an input with such a name is not found the corresponding entry is skipped.
+ *
+ * @param data Map containing data to fill form inputs with.
+ */
+ public void fill(Map data) {
+ data.entrySet().stream()
+ .map(e -> new AbstractMap.SimpleEntry<>(
+ findElementByKey(e.getKey()),
+ Objects.toString(e.getValue(), "")))
+ .filter(e -> !isNull(e.getKey()))
+ .forEach(e -> fillElement(e.getKey(), e.getValue()));
+ }
+
+ protected WebElement findElementByKey(String key) {
+ List elements = getWrappedElement().findElements(By.name(key));
+ if (elements.isEmpty()) {
+ return null;
+ } else {
+ return elements.get(0);
+ }
+ }
+
+ protected void fillElement(WebElement element, String value) {
+ String elementType = getElementType(element);
+
+ if (CHECKBOX_FIELD.equals(elementType)) {
+ fillCheckBox(element, value);
+ } else if (RADIO_FIELD.equals(elementType)) {
+ fillRadio(element, value);
+ } else if (INPUT_FIELD.equals(elementType)) {
+ fillInput(element, value);
+ } else if (SELECT_FIELD.equals(elementType)) {
+ fillSelect(element, value);
+ } else if (FILE_FIELD.equals(elementType)) {
+ fillFile(element, value);
+ }
+ }
+
+ protected String getElementType(WebElement element) {
+ String tagName = element.getTagName();
+ if ("input".equals(tagName)) {
+ String type = element.getAttribute("type");
+ if ("checkbox".equals(type)) {
+ return CHECKBOX_FIELD;
+ } else if ("radio".equals(type)) {
+ return RADIO_FIELD;
+ } else if ("file".equals(type)) {
+ return FILE_FIELD;
+ } else {
+ return INPUT_FIELD;
+ }
+ } else if ("select".equals(tagName)) {
+ return SELECT_FIELD;
+ } else if ("textarea".equals(tagName)) {
+ return INPUT_FIELD;
+ } else {
+ return null;
+ }
+ }
+
+ protected void fillCheckBox(WebElement element, String value) {
+ new CheckBox(element).set(Boolean.parseBoolean(value));
+ }
+
+ protected void fillRadio(WebElement element, String value) {
+ new Radio(element).selectByValue(value);
+ }
+
+ protected void fillInput(WebElement element, String value) {
+ TextInput input = new TextInput(element);
+ input.sendKeys(input.getClearCharSequence() + value);
+ }
+
+ protected void fillSelect(WebElement element, String value) {
+ new Select(element).selectByValue(value);
+ }
+
+ protected void fillFile(WebElement element, String value) {
+ new FileInput(element).setFileToUpload(value);
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/HtmlElement.java b/src/main/java/com/frameworkium/core/htmlelements/element/HtmlElement.java
new file mode 100755
index 00000000..2b2da1e4
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/HtmlElement.java
@@ -0,0 +1,165 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.*;
+
+import java.util.List;
+
+/**
+ * The base class to be used for making blocks of elements.
+ *
+ * To make a class that will represent a block of elements (e.g. web form)
+ * create a descendant of this class and fill it with elements.
+ *
+ * For example:
+ *
+ *
+ * @FindBy(css = "form_css")
+ * public class SearchForm extends HtmlElement {
+ *
+ * @FindBy(css = "request_input_css")
+ * private TextInput requestInput;
+ *
+ * @FindBy(css = "search_button_css")
+ * private Button searchButton;
+ *
+ * public TextInput getRequestInput() {
+ * return requestInput;
+ * }
+ *
+ * public Button getSearchButton() {
+ * return searchButton;
+ * }
+ * }
+ *
+ *
+ * Then you can use created blocks as fields of page objects or you can also
+ * initialize them directly with methods of
+ * {@link com.frameworkium.core.htmlelements.loader.HtmlElementLoader} class.
+ *
+ * Note that this class implements {@link WebElement} interface so you can
+ * substitute instances of your block classes for
+ * {@code WebElements} where necessary.
+ */
+public class HtmlElement implements WebElement, WrapsElement {
+ private WebElement wrappedElement;
+
+ @Override
+ public WebElement getWrappedElement() {
+ return wrappedElement;
+ }
+
+ /**
+ * Sets the wrapped {@code WebElement}. This method is used by
+ * initialization mechanism and is not intended to be used directly.
+ *
+ * @param wrappedElement {@code WebElement} to wrap.
+ */
+ public void setWrappedElement(WebElement wrappedElement) {
+ this.wrappedElement = wrappedElement;
+ }
+
+ /**
+ * Determines whether or not this element exists on page.
+ *
+ * @return True if the element exists on page, false otherwise.
+ */
+ public boolean exists() {
+ try {
+ getWrappedElement().isDisplayed();
+ } catch (NoSuchElementException ignored) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void click() {
+ wrappedElement.click();
+ }
+
+ /**
+ * If this element is a form, or an element within a form, then this will
+ * submit this form to the remote server.
+ *
+ * @see WebElement#submit()
+ */
+ @Override
+ public void submit() {
+ wrappedElement.submit();
+ }
+
+ @Override
+ public void sendKeys(CharSequence... charSequences) {
+ wrappedElement.sendKeys(charSequences);
+ }
+
+ @Override
+ public void clear() {
+ wrappedElement.clear();
+ }
+
+ @Override
+ public String getTagName() {
+ return wrappedElement.getTagName();
+ }
+
+ @Override
+ public String getAttribute(String name) {
+ return wrappedElement.getAttribute(name);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return wrappedElement.isSelected();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return wrappedElement.isEnabled();
+ }
+
+ @Override
+ public String getText() {
+ return wrappedElement.getText();
+ }
+
+ @Override
+ public List findElements(By by) {
+ return wrappedElement.findElements(by);
+ }
+
+ @Override
+ public WebElement findElement(By by) {
+ return wrappedElement.findElement(by);
+ }
+
+ @Override
+ public boolean isDisplayed() {
+ return wrappedElement.isDisplayed();
+ }
+
+ @Override
+ public Point getLocation() {
+ return wrappedElement.getLocation();
+ }
+
+ @Override
+ public Dimension getSize() {
+ return wrappedElement.getSize();
+ }
+
+ @Override
+ public Rectangle getRect() {
+ return wrappedElement.getRect();
+ }
+
+ @Override
+ public String getCssValue(String name) {
+ return wrappedElement.getCssValue(name);
+ }
+
+ @Override
+ public X getScreenshotAs(OutputType outputType) throws WebDriverException {
+ return wrappedElement.getScreenshotAs(outputType);
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Image.java b/src/main/java/com/frameworkium/core/htmlelements/element/Image.java
new file mode 100755
index 00000000..bfd64010
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Image.java
@@ -0,0 +1,30 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.WebElement;
+
+/** Represents an image {@code
} */
+public class Image extends TypifiedElement {
+
+ public Image(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Retrieves path to image from "src" attribute
+ *
+ * @return Path to the image
+ */
+ public String getSource() {
+ return getWrappedElement().getAttribute("src");
+ }
+
+ /**
+ * Retrieves alternative text from "alt" attribute
+ *
+ * @return alternative text for image
+ */
+ public String getAlt() {
+ return getWrappedElement().getAttribute("alt");
+ }
+
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Link.java b/src/main/java/com/frameworkium/core/htmlelements/element/Link.java
new file mode 100755
index 00000000..9147f247
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Link.java
@@ -0,0 +1,20 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.WebElement;
+
+/** Represents an anchor tag/hyperlink. */
+public class Link extends TypifiedElement {
+
+ public Link(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Retrieves reference from "href" attribute.
+ *
+ * @return Reference associated with hyperlink.
+ */
+ public String getReference() {
+ return getWrappedElement().getAttribute("href");
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Radio.java b/src/main/java/com/frameworkium/core/htmlelements/element/Radio.java
new file mode 100755
index 00000000..e96781de
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Radio.java
@@ -0,0 +1,97 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.*;
+
+import java.util.List;
+import java.util.Optional;
+
+/** Represents a group of radio buttons. */
+public class Radio extends TypifiedElement {
+
+ /**
+ * Specifies a radio button of a radio button group that will be used
+ * to find all other buttons of this group.
+ *
+ * @param wrappedElement {@code WebElement} representing radio button.
+ */
+ public Radio(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Returns all radio buttons belonging to this group.
+ *
+ * @return A list of {@code WebElements} representing radio buttons.
+ */
+ public List getButtons() {
+ String radioName = getWrappedElement().getAttribute("name");
+
+ String xpath;
+ if (radioName == null) {
+ xpath = "self::* | following::input[@type = 'radio'] | preceding::input[@type = 'radio']";
+ } else {
+ xpath = String.format(
+ "self::* | following::input[@type = 'radio' and @name = '%s'] | "
+ + "preceding::input[@type = 'radio' and @name = '%s']",
+ radioName, radioName);
+ }
+
+ return getWrappedElement().findElements(By.xpath(xpath));
+ }
+
+ /**
+ * Returns selected radio button.
+ *
+ * @return Optional {@code WebElement} representing selected radio button.
+ */
+ public Optional getSelectedButton() {
+ return getButtons().stream()
+ .filter(WebElement::isSelected)
+ .findAny();
+ }
+
+ /**
+ * Indicates if radio group has selected button.
+ *
+ * @return {@code true} if radio has selected button and {@code false} otherwise.
+ */
+ public boolean hasSelectedButton() {
+ return getButtons().stream()
+ .anyMatch(WebElement::isSelected);
+ }
+
+ /**
+ * Selects first radio button that has a value matching the specified argument.
+ *
+ * @param value The value to match against.
+ */
+ public void selectByValue(String value) {
+ WebElement matchingButton = getButtons().stream()
+ .filter(b -> value.equals(b.getAttribute("value")))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(
+ String.format("Cannot locate radio button with value: %s", value)));
+
+ selectButton(matchingButton);
+ }
+
+ /**
+ * Selects a radio button by the given index.
+ *
+ * @param index Index of a radio button to be selected.
+ */
+ public void selectByIndex(int index) {
+ selectButton(getButtons().get(index));
+ }
+
+ /**
+ * Selects a radio button if it's not already selected.
+ *
+ * @param button {@code WebElement} representing radio button to be selected.
+ */
+ private void selectButton(WebElement button) {
+ if (!button.isSelected()) {
+ button.click();
+ }
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Select.java b/src/main/java/com/frameworkium/core/htmlelements/element/Select.java
new file mode 100755
index 00000000..31e52e88
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Select.java
@@ -0,0 +1,93 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ISelect;
+
+import java.util.List;
+
+/**
+ * Represents a select control.
+ *
+ * This class wraps the Selenium {@link org.openqa.selenium.support.ui.Select}
+ * and delegates all method calls to it.
+ *
+ * But unlike {@code WebDriver} {@code Select} class there are no checks
+ * performed in the constructor of this class, so it can be used correctly
+ * with lazy initialization.
+ */
+public class Select extends TypifiedElement implements ISelect {
+
+ /**
+ * Specifies wrapped {@link WebElement}.
+ * Performs no checks unlike {@link org.openqa.selenium.support.ui.Select}.
+ * All checks are made later in {@link #getSelect()} method.
+ *
+ * @param wrappedElement {@code WebElement} to wrap.
+ */
+ public Select(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Constructs instance of {@link org.openqa.selenium.support.ui.Select} class.
+ *
+ * @return {@link org.openqa.selenium.support.ui.Select} class instance.
+ */
+ private org.openqa.selenium.support.ui.Select getSelect() {
+ return new org.openqa.selenium.support.ui.Select(getWrappedElement());
+ }
+
+ public boolean isMultiple() {
+ return getSelect().isMultiple();
+ }
+
+ public List getOptions() {
+ return getSelect().getOptions();
+ }
+
+ public List getAllSelectedOptions() {
+ return getSelect().getAllSelectedOptions();
+ }
+
+ public WebElement getFirstSelectedOption() {
+ return getSelect().getFirstSelectedOption();
+ }
+
+ /**
+ * Indicates if select has at least one selected option.
+ *
+ * @return {@code true} if select has at least one selected option and
+ * {@code false} otherwise.
+ */
+ public boolean hasSelectedOption() {
+ return getOptions().stream().anyMatch(WebElement::isSelected);
+ }
+
+ public void selectByVisibleText(String text) {
+ getSelect().selectByVisibleText(text);
+ }
+
+ public void selectByIndex(int index) {
+ getSelect().selectByIndex(index);
+ }
+
+ public void selectByValue(String value) {
+ getSelect().selectByValue(value);
+ }
+
+ public void deselectAll() {
+ getSelect().deselectAll();
+ }
+
+ public void deselectByValue(String value) {
+ getSelect().deselectByValue(value);
+ }
+
+ public void deselectByIndex(int index) {
+ getSelect().deselectByIndex(index);
+ }
+
+ public void deselectByVisibleText(String text) {
+ getSelect().deselectByVisibleText(text);
+ }
+}
diff --git a/src/main/java/com/frameworkium/core/htmlelements/element/Table.java b/src/main/java/com/frameworkium/core/htmlelements/element/Table.java
new file mode 100755
index 00000000..7a0f7ce7
--- /dev/null
+++ b/src/main/java/com/frameworkium/core/htmlelements/element/Table.java
@@ -0,0 +1,185 @@
+package com.frameworkium.core.htmlelements.element;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.*;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * Represents a simple table element.
+ * Provides convenient ways of retrieving data stored in it.
+ *
+ * @see com.frameworkium.core.ui.element.AbstractStreamTable
+ * @see com.frameworkium.core.ui.element.StreamTable
+ * @see com.frameworkium.core.ui.element.OptimisedStreamTable
+ */
+public class Table extends TypifiedElement {
+
+ /**
+ * Specifies {@link WebElement} representing table tag.
+ *
+ * @param wrappedElement {@code WebElement} to wrap.
+ */
+ public Table(WebElement wrappedElement) {
+ super(wrappedElement);
+ }
+
+ /**
+ * Returns a list of table heading elements ({@code | }).
+ *
+ * Multiple rows of heading elements, ({@code | }), are flattened
+ * i.e. the second row, ({@code
}), will follow the first, which can be
+ * misleading when the table uses {@code colspan} and {@code rowspan}.
+ *
+ * @return List with table heading elements.
+ */
+ public List getHeadings() {
+ return getWrappedElement().findElements(By.xpath(".//th"));
+ }
+
+ /**
+ * Returns text values of table heading elements (contained in "th" tags).
+ *
+ * @return List with text values of table heading elements.
+ */
+ public List getHeadingsAsString() {
+ return getHeadings().stream()
+ .map(WebElement::getText)
+ .collect(toList());
+ }
+
+ /**
+ * Returns table cell elements ({@code }), grouped by rows.
+ *
+ * @return List where each item is a table row.
+ */
+ public List> getRows() {
+ return getWrappedElement()
+ .findElements(By.xpath(".//tr"))
+ .stream()
+ .map(rowElement -> rowElement.findElements(By.xpath(".//td")))
+ .filter(row -> row.size() > 0) // ignore rows with no | tags
+ .collect(toList());
+ }
+
+ /**
+ * Returns text values of table cell elements ({@code | }), grouped by rows.
+ *
+ * @return List where each item is text values of a table row.
+ */
+ public List> getRowsAsString() {
+ return getRows().stream()
+ .map(row -> row.stream()
+ .map(WebElement::getText)
+ .collect(toList()))
+ .collect(toList());
+ }
+
+ /**
+ * Returns table cell elements ({@code }), grouped by columns.
+ *
+ * @return List where each item is a table column.
+ */
+ public List> getColumns() {
+ List> columns = new ArrayList<>();
+ List> rows = getRows();
+
+ if (rows.isEmpty()) {
+ return columns;
+ }
+
+ int columnCount = rows.get(0).size();
+ for (int i = 0; i < columnCount; i++) {
+ List column = new ArrayList<>();
+ for (List row : rows) {
+ column.add(row.get(i));
+ }
+ columns.add(column);
+ }
+
+ return columns;
+ }
+
+ /**
+ * Returns table cell elements ({@code }), of a particular column.
+ *
+ * @param index the 1-based index of the desired column
+ * @return List where each item is a cell of a particular column.
+ */
+ public List getColumnByIndex(int index) {
+ return getWrappedElement().findElements(
+ By.cssSelector(String.format("tr > td:nth-of-type(%d)", index)));
+ }
+
+ /**
+ * Returns text values of table cell elements ({@code }), grouped by columns.
+ *
+ * @return List where each item is text values of a table column.
+ */
+ public List> getColumnsAsString() {
+ return getColumns().stream()
+ .map(row -> row.stream()
+ .map(WebElement::getText)
+ .collect(toList()))
+ .collect(toList());
+ }
+
+ /**
+ * Returns table cell element ({@code | }), at i-th row and j-th column.
+ *
+ * @param i Row number
+ * @param j Column number
+ * @return Cell element at i-th row and j-th column.
+ */
+ public WebElement getCellAt(int i, int j) {
+ return getRows().get(i).get(j);
+ }
+
+ /**
+ * Returns list of maps where keys are table headings and values are table row elements ({@code | }).
+ */
+ public List | | | | | |