package com.abmash.REMOVE.core.htmlquery.condition; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.lang.StringUtils; import com.abmash.REMOVE.core.htmlquery.condition.ClosenessCondition.Direction; import com.abmash.REMOVE.core.htmlquery.selector.CssSelector; import com.abmash.REMOVE.core.htmlquery.selector.DirectMatchSelector; import com.abmash.REMOVE.core.htmlquery.selector.JQuerySelector; import com.abmash.REMOVE.core.htmlquery.selector.Selector; import com.abmash.REMOVE.core.htmlquery.selector.SelectorGroup; import com.abmash.REMOVE.core.htmlquery.selector.XpathSelector; import com.abmash.REMOVE.core.htmlquery.selector.SelectorGroup.Type; import com.abmash.api.Browser; import com.abmash.api.HtmlElement; import com.abmash.api.HtmlElements; public class ElementCondition extends Condition { /** * Element type for this condition */ public enum ElementType { ALL, TEXT, TITLE, CLICKABLE, TYPABLE, CHOOSABLE, DATEPICKER, IMAGE, LIST, TABLE, INLIST, INTABLE, FRAME, } private Browser browser; private ElementType elementType; private ArrayList<String> queryStrings = new ArrayList<String>(); // constructors public ElementCondition(Browser browser, ElementType elementType) { this.browser = browser; this.elementType = elementType; } public void addQueries(List<String> queries) { for(String query: queries) { addQuery(query); } } public void addQuery(String query) { if(query != null && !query.equals("") && !queryStrings.contains(query)) queryStrings.add(query); } // condition @Override protected void buildSelectors() { switch (elementType) { case ALL: selectorsForAll(); break; case TEXT: selectorsForText(); break; case TITLE: selectorsForTitles(); break; case CLICKABLE: selectorsForClickables(); break; case TYPABLE: selectorsForTypables(); break; case CHOOSABLE: selectorsForChoosables(); break; case DATEPICKER: selectorsForDatepickers(); break; case IMAGE: selectorsForImages(); break; case LIST: selectorsForLists(); break; case TABLE: selectorsForTables(); break; case INLIST: selectorsInLists(); break; case INTABLE: selectorsInTables(); break; case FRAME: selectorsForFrames(); break; } } private void selectorsForAll() { List<String> elementNames = Arrays.asList("*"); List<String> attributeNames = Arrays.asList("*"); // find target by inner text and html attributes which match exactly or partially selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS), "input")); selectorGroups.add(checkElementAttributes("*", queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS), "input")); // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if nothing was found just return all elements // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK, 1)); // } } private void selectorsForText() { // TODO bevorzugung von bestimmten attributen innerhalb des jeweiligen elements List<String> elementNames = Arrays.asList("strong", "em", "b", "i", "label", "span", "p", "div", "li", "td", "th", "*"); List<String> attributeNames = Arrays.asList("for", "id", "class", "name", "value", "title", "alt", "*"); // find target by inner text and html attributes which match exactly or partially selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS), "input")); selectorGroups.add(checkElementAttributes("*", queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS), "input")); // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if nothing was found just return all elements // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK, 1)); // } } private void selectorsForTitles() { List<String> elementNames = Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6", "*"); List<String> attributeNames = Arrays.asList("for", "id", "class", "name", "value", "type", "title", "alt", "*"); // find target by inner text which match exactly or partially selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); // find elements with font-size bigger than the default // TODO move that to jquery // TODO combine css check with text check // HtmlElement body = browser.query().tag("body").findFirst(); // selectorGroups.add(checkElementCssAttribute("*", Arrays.asList(body.getCssValue("font-size")), "font-size", CSSAttributeMatcher.GREATER_THAN)); // find target by html attributes which match exactly or partially selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS))); // find target by tag name selector // TODO tagname selector necessary? // TODO which query? /*if(queryStrings != null && !queryStrings.equals("")) { selectorGroups.add(new SelectorGroup(new TagnameSelector(queryStrings))); }*/ // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if nothing was found just return all headline elements // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForClickables() { // TODO with onclick event handler List<String> linkNames = Arrays.asList("a", "*[onclick]"); List<String> inputNames = Arrays.asList("input[type='checkbox']", "input[type='radio']", "input[type='submit']", "input[type='button']", "input[type='image']", "input[type='range']", "input[type='color']", "button"); List<String> elementNames = new ArrayList<String>(linkNames); elementNames.addAll(inputNames); List<String> attributeNames = Arrays.asList("title", "alt", "href", "id", "class", "*"); // find links, checkboxes, radioboxes and buttons selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // find any element by inner text selectorGroups.add(checkElementText(linkNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); // find clickables by searching for closest fitting label // if(!queryStrings.isEmpty()) { // HtmlElements labelElements = browser.query().isText().has(queryStrings).limit(2).find(); // if(!labelElements.isEmpty()) { // selectorGroups.addLabelElements(labelElements); // selectorGroups.setLabelType(Direction.SELECT); // // now add all checkboxes for closest label comparison // selectorGroups.add(new SelectorGroup(new JQuerySelector("find('input[type=checkbox], input[type=radio]')", Weight.VERYLOW.getValue()), Type.LABEL)); // } // } // TODO elements with parent <a> tags are also clickable if(queryStrings.isEmpty()) { // TODO has-parent instead of following-sibling // selectors.add(new SelectorGroup(new XpathSelector("//a[contains(" + xpathToLowercase + ", '" + textQuery.toLowerCase() + "')]/following-sibling::input"))); } else { // TODO has-parent without text } // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if there were no results just find all clickables // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForTypables() { // TODO without check and radioboxes List<String> elementNames = Arrays.asList( "input[type=password]", "input[type=text]", "input[type=email]", "input[type=url]", "input[type=number]", "input[type=search]", "textarea", "input"); List<String> attributeNames = Arrays.asList("id", "name", "value", "class", "type", "title", "*"); // find target by html attributes which match exactly selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, AttributeMatcher.EXACT)); // find tinyMce editor iframes // TODO move to JavaScript Selector tinymceSelector = new CssSelector(".mceIframeContainer iframe[id*='" + queryStrings + "']"); try { // TODO find with rootelements if needed if(tinymceSelector.find(browser).isEmpty()) { tinymceSelector = new JQuerySelector("find('.mceIframeContainer iframe:attrCaseInsensitive(CONTAINS, id, " + queryStrings + ")')"); } HtmlElements tinymceElements = tinymceSelector.find(browser); if(tinymceElements != null && !tinymceElements.isEmpty()) { HtmlElement frame = tinymceElements.first(); browser.frame().switchTo(frame); // TODO find with rootelements if needed // HtmlElement textarea = browser.query().cssSelector("#tinymce").findFirst(); // if(textarea instanceof HtmlElement) { // textarea.setFrameElement(frame); // selectorGroups.add(new SelectorGroup(new DirectMatchSelector(new HtmlElements(textarea)))); // } // TODO switch to previously focused main content browser.window().switchToMainContent(); } } catch (Exception e) { // if no element was found, just continue with next selectors } // TODO find inputs with fitting labels by "id" and "for" // find inputs by searching for closest fitting label // if(!queryStrings.isEmpty()) { // HtmlElements labelElements = browser.query().isText().has(queryStrings).limit(2).find(); // if(!labelElements.isEmpty()) { // selectorGroups.addLabelElements(labelElements); // } // } // find target by html attributes which match partially selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS))); // find labels and other elements containing the query and return the next descendant input element for (String query: queryStrings) { // TODO combine all query strings! // TODO with textarea too selectorGroups.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + query.toLowerCase() + "')]/following-sibling::input"))); selectorGroups.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + query.toLowerCase() + "')]/following-sibling::textarea"))); // selectors.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + textQuery.toLowerCase() + "')]/ancestor::div/descendant::input"))); // selectors.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + textQuery.toLowerCase() + "')]/ancestor::ul/descendant::input"))); // selectors.add(new SelectorGroup(new XpathSelector("//*[contains(" + xpathToLowercase + ", '" + textQuery.toLowerCase() + "')]/ancestor::div/descendant::input"))); // selectors.add(new SelectorGroup(new XpathSelector("//*[contains(" + xpathToLowercase + ", '" + textQuery.toLowerCase() + "')]/ancestor::ul/descendant::input"))); } // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if there were no results just find all inputs // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForChoosables() { List<String> elementNames = Arrays.asList("select"); List<String> attributeNames = Arrays.asList("id", "class", "name", "value", "*"); // find target by text and html attributes selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // find target by option labels selectorGroups.add(new SelectorGroup(new JQuerySelector("find('select:has(option:containsCaseInsensitive(" + queryStrings + "))')"), Weight.QUITE.getValue())); // TODO option attributes // if there were no results just find all selects selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // if(queryLimit != 2) alert(selector.command.toSource()); } private void selectorsForDatepickers() { List<String> elementNames = Arrays.asList("input[type=date], input"); List<String> attributeNames = Arrays.asList("id", "class", "name"); // find target by option labels //selectors.add(new SelectorGroup(new JQuerySelector("find('select:has(option:containsCaseInsensitive(" + textQuery + "))')"))); // find datepickers by searching for closest fitting label // if(!queryStrings.isEmpty()) { // HtmlElements labelElements = browser.query().isText().has(queryStrings).limit(2).find(); // if(!labelElements.isEmpty()) { // selectorGroups.addLabelElements(labelElements); // } // } // find target by html attributes selectorGroups.add(checkElementAttributes(elementNames, Arrays.asList("datepicker"), attributeNames, Arrays.asList(AttributeMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, Arrays.asList("calendar"), attributeNames, Arrays.asList(AttributeMatcher.CONTAINS))); for (String query: queryStrings) { // TODO combine all query strings! selectorGroups.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + query.toLowerCase() + "')]/following-sibling::input"))); selectorGroups.add(new SelectorGroup(new XpathSelector("//label[contains(" + xpathToLowercase + ", '" + query.toLowerCase() + "')]/following-sibling::input"))); } // if there were no results just find all datepickers // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForImages() { List<String> elementNames = Arrays.asList("img"); List<String> attributeNames = Arrays.asList("id", "title", "alt", "class", "name", "*"); // find exact image matches selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT))); selectorGroups.add(checkElementCssAttribute("*", queryStrings, "background-image", Arrays.asList(CSSAttributeMatcher.EQUAL))); // find target by html attributes selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // CSS background images with check if their url match the given query strings // exists also as fallback without query string matching selectorGroups.add(checkElementCssAttribute("*", queryStrings, "background-image", Arrays.asList(CSSAttributeMatcher.CONTAINS))); // if there were no results just find all images and elements with background images SelectorGroup group = new SelectorGroup(Type.FALLBACK); group.add(new CssSelector(StringUtils.join(elementNames, ','))); // TODO also as fallback if query strings were specified? group.addAll(checkElementCssAttribute("*", Arrays.asList("none"), "background-image", Arrays.asList(CSSAttributeMatcher.NOT_EQUAL))); selectorGroups.add(group); } private void selectorsForLists() { List<String> elementNames = Arrays.asList("ul,ol,dl"); List<String> attributeNames = Arrays.asList("id", "class", "name", "*"); // find target by text and html attributes selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // if there were no results just find all lists // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForTables() { List<String> elementNames = Arrays.asList("table"); List<String> attributeNames = Arrays.asList("id", "class", "name", "*"); // find target by text and html attributes selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // if there were no results just find all tables if(!queryStrings.isEmpty()) { selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); } } private void selectorsInLists() { // TODO dl besteht aus dt und dd (term/descriptions) List<String> elementNames = Arrays.asList("li, dt"); List<String> attributeNames = Arrays.asList("id", "class", "name", "value", "title", "alt", "for", "*"); // find target by inner text and html attributes which match exactly or partially selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS))); // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if there were no results just find list items // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsInTables() { List<String> elementNames = Arrays.asList("td,th"); List<String> attributeNames = Arrays.asList("id", "class", "name", "value", "title", "alt", "for", "*"); // find target by inner text and html attributes which match exactly or partially selectorGroups.add(checkElementText(elementNames, queryStrings, Arrays.asList(TextMatcher.EXACT, TextMatcher.CONTAINS))); selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.STARTSWITH, AttributeMatcher.ENDSWITH, AttributeMatcher.CONTAINS))); // find target by case insensitive attribute matches /*if(queryStrings != null && !queryStrings.equals("") && !queryStrings.contains("[") && !queryStrings.contains("]") && !queryStrings.contains("(") && !queryStrings.contains(")")) { SelectorGroup group = new SelectorGroup(Type.FALLBACK, 1); group.add(new JQuerySelector("find('*:attrCaseInsensitive(EXACT, *, " + queryStrings + ")')")); group.add(new JQuerySelector("find('*:attrCaseInsensitive(CONTAINS, *, " + queryStrings + ")')")); selectorGroups.add(group); }*/ // if there were no results just find all table cells // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } private void selectorsForFrames() { List<String> elementNames = Arrays.asList("frame", "iframe"); List<String> attributeNames = Arrays.asList("id", "name", "class", "title", "alt", "*"); // find target by html attributes selectorGroups.add(checkElementAttributes(elementNames, queryStrings, attributeNames, Arrays.asList(AttributeMatcher.EXACT, AttributeMatcher.WORD, AttributeMatcher.CONTAINS))); // if there were no results just find all frames // if(!queryStrings.isEmpty()) { // selectorGroups.add(new SelectorGroup(new CssSelector(StringUtils.join(elementNames, ',')), Type.FALLBACK)); // } } public boolean elementValid(HtmlElement foundElement) { if(!super.elementValid(foundElement)) return false; // TODO move to JavaScript // if(elementType == ElementType.TITLE && foundElement.getText().isEmpty()) return false; return true; } public ElementType getElementType() { return elementType; } public String toString() { return super.toString() + " with type [" + elementType + "]" + (!queryStrings.isEmpty() ? " and text queries [" + queryStrings + "]" : ""); } }