001: // Copyright © 2002-2007 Canoo Engineering AG, Switzerland.
002: package com.canoo.webtest.steps.request;
003:
004: import com.canoo.webtest.engine.StepFailedException;
005: import com.canoo.webtest.util.ConversionUtil;
006: import com.canoo.webtest.util.HtmlConstants;
007: import com.gargoylesoftware.htmlunit.Page;
008: import com.gargoylesoftware.htmlunit.html.ClickableElement;
009: import com.gargoylesoftware.htmlunit.html.HtmlButton;
010: import com.gargoylesoftware.htmlunit.html.HtmlElement;
011: import com.gargoylesoftware.htmlunit.html.HtmlForm;
012: import com.gargoylesoftware.htmlunit.html.HtmlImageInput;
013: import com.gargoylesoftware.htmlunit.html.HtmlInput;
014: import com.gargoylesoftware.htmlunit.html.HtmlPage;
015: import org.apache.commons.lang.StringUtils;
016: import org.apache.log4j.Logger;
017: import org.jaxen.JaxenException;
018: import org.xml.sax.SAXException;
019:
020: import java.io.IOException;
021: import java.util.ArrayList;
022: import java.util.Collection;
023: import java.util.HashSet;
024: import java.util.Iterator;
025: import java.util.List;
026: import java.util.Set;
027:
028: /**
029: * Provides the ability to click on a submit button.
030: *
031: * @author unknown
032: * @author Marc Guillemot
033: * @author Paul King
034: * @author Denis N. Antonioli
035: * @webtest.step category="Core"
036: * name="clickButton"
037: * alias="clickbutton"
038: * description="This step is used to locate a form button and then click it."
039: */
040: public class ClickButton extends AbstractIdOrLabelTarget {
041: private static final Logger LOG = Logger
042: .getLogger(ClickButton.class);
043: private static final Set INPUT_BUTTONS_TYPES = new HashSet();
044: private String fName;
045: private String fFieldIndex;
046: private String fX;
047: private String fY;
048:
049: static {
050: INPUT_BUTTONS_TYPES.add(HtmlConstants.SUBMIT);
051: INPUT_BUTTONS_TYPES.add(HtmlConstants.IMAGE);
052: INPUT_BUTTONS_TYPES.add(HtmlConstants.BUTTON);
053: INPUT_BUTTONS_TYPES.add(HtmlConstants.RESET);
054: }
055:
056: /**
057: * @webtest.parameter required="yes/no"
058: * description="The NAME attribute for the button of interest.
059: * One of 'label', 'name', 'htmlid' or 'xpath' must be set.
060: * Name has lower precedence than <em>htmlId</em>."
061: */
062: public void setName(String name) {
063: fName = name;
064: }
065:
066: public String getName() {
067: return fName;
068: }
069:
070: /**
071: * Sets the index of the button to click (starting with 0) within the buttons
072: * identified with the other criteria.
073: *
074: * @param index the new value
075: * @webtest.parameter required="no"
076: * default="0"
077: * description="The index (starting with 0) of the button to click within the buttons having the specified label and/or name. Useful for instance to distinguish two buttons having the same name."
078: */
079: public void setFieldIndex(final String index) {
080: fFieldIndex = index;
081: }
082:
083: public String getFieldIndex() {
084: return fFieldIndex;
085: }
086:
087: /**
088: * @webtest.parameter required="no"
089: * description="Optional X coordinate of click within an image button. If set, Y coordinate must also be set."
090: */
091: public void setX(String clickPositionX) {
092: fX = clickPositionX;
093: }
094:
095: public String getX() {
096: return fX;
097: }
098:
099: /**
100: * @webtest.parameter required="no"
101: * description="Optional Y coordinate of click within an image button. If set, X coordinate must also be set."
102: */
103: public void setY(String clickPositionY) {
104: fY = clickPositionY;
105: }
106:
107: public String getY() {
108: return fY;
109: }
110:
111: /**
112: * @deprecated use setFieldIndex instead
113: */
114:
115: public void setIndex(final int index) {
116: LOG.warn("setIndex is deprecated - use setFieldIndex instead");
117: setFieldIndex(Integer.toString(index));
118: }
119:
120: /**
121: * Finds the button in the page according to the properties set on this step
122: *
123: * @param page the page to search in
124: * @return the button, <code>null</code> if not found
125: */
126: protected ClickableElement findClickableElementByAttribute(
127: final HtmlPage page) {
128: ClickableElement button = null;
129: // look for the button in the current form
130: if (getContext().getCurrentForm() != null) {
131: LOG.debug("Looking for button in current form");
132: button = findButton(getContext().getCurrentForm());
133: }
134: // if not found look at the other forms
135: if (button == null) {
136: button = findButtonAllForms(page);
137: }
138: return button;
139: }
140:
141: protected Page findTarget() throws JaxenException, IOException,
142: SAXException {
143: final ClickableElement button = findClickableElement(getContext()
144: .getCurrentHtmlResponse(this ));
145: if (button == null) {
146: throw buildNoButtonFoundException();
147: }
148:
149: LOG.info("-> findTarget(by " + button.getTagName() + "): name="
150: + button.getAttributeValue("name") + " value="
151: + button.getAttributeValue("value"));
152:
153: if (isImageButton()) {
154: LOG.info("-> findTarget(by " + button.getTagName()
155: + "): name=" + button.getAttributeValue("name")
156: + " value=" + button.getAttributeValue("value"));
157: return ((HtmlInput) button).click(Integer.parseInt(getX()),
158: Integer.parseInt(getY()));
159: }
160: return button.click();
161: }
162:
163: /**
164: * Builds an exception with helpfull information
165: * @return the exception
166: */
167: private StepFailedException buildNoButtonFoundException() {
168: final StepFailedException e = new StepFailedException(
169: "No button found", this );
170:
171: final HtmlForm currentForm = getContext().getCurrentForm();
172: final StringBuffer msg = new StringBuffer();
173: if (currentForm != null) {
174: msg.append("In current form:\n");
175: msg.append(getButtonsDescription(currentForm));
176: }
177:
178: final Iterator formsIterator = getContext()
179: .getCurrentHtmlResponse(this ).getForms().iterator();
180: while (formsIterator.hasNext()) {
181: final HtmlForm form = (HtmlForm) formsIterator.next();
182: if (form != currentForm) {
183: if (msg.length() != 0)
184: msg.append("\n\n");
185: msg.append("In " + form + ":\n");
186: msg.append(getButtonsDescription(form));
187: }
188: }
189:
190: e.addDetail("available buttons", msg.toString());
191: return e;
192: }
193:
194: private String getButtonsDescription(final HtmlForm _form) {
195: final List buttons = new ArrayList();
196: for (final Iterator iter = _form.getAllHtmlChildElements(); iter
197: .hasNext();) {
198: final HtmlElement element = (HtmlElement) iter.next();
199: if ((element instanceof HtmlInput)
200: && isInputButtonType((HtmlInput) element)) {
201: buttons.add(element);
202: } else if (element instanceof HtmlButton) {
203: buttons.add(element);
204: }
205: }
206:
207: if (buttons.isEmpty())
208: return "none";
209: else
210: return buttons.toString();
211: }
212:
213: protected String getLogMessageForTarget() {
214: return "by clickButton with name: " + getName();
215: }
216:
217: protected void verifyParameters() {
218: super .verifyParameters();
219: nullResponseCheck();
220:
221: paramCheck((StringUtils.isEmpty(getX()) && !StringUtils
222: .isEmpty(getY()))
223: || (!StringUtils.isEmpty(getX()) && StringUtils
224: .isEmpty(getY())),
225: "X and Y values must be set for click button support!");
226: paramCheck(getLabel() == null && fName == null
227: && getHtmlId() == null && getXpath() == null,
228: "Required parameter 'label', 'name', 'htmlid' or 'xpath' must be set!");
229:
230: optionalIntegerParamCheck(getFieldIndex(), "fieldIndex", true);
231: optionalIntegerParamCheck(getX(), "x", false);
232: optionalIntegerParamCheck(getY(), "y", false);
233: }
234:
235: /**
236: * Checks that the element is of the desired html type and has the right name and label (if needed)
237: *
238: * @param elt the button to check
239: * @return the button if ok, <code>null</code> otherwise
240: */
241: ClickableElement checkFoundElement(final HtmlElement elt)
242: throws StepFailedException {
243: // check that it is a "button"
244: if (!isButton(elt)) {
245: throw new StepFailedException("Selected element is a "
246: + elt.getTagName() + " tag and not a button", this );
247: }
248:
249: if (hasMatchingNameOrDontCare(elt)
250: && hasMatchingLabelOrDontCare(elt)) {
251: LOG.debug("Button passes test with label and name");
252: return (ClickableElement) elt;
253: }
254: LOG.debug("Test with name and label fails for html button: "
255: + elt);
256: return null;
257: }
258:
259: /**
260: * Looks for the first button (that may be an input of type submit, image or button or a "normal" button)
261: * in the given form corresponding to the criterias
262: *
263: * @param form the form in which the button should be searched
264: * @return the button, <code>null</code> if not found
265: */
266: ClickableElement findButton(final HtmlForm form) {
267: LOG
268: .debug("Looking for inputs of type submit, image or button in "
269: + form);
270: ClickableElement button = findInputButton(form);
271: if (button != null) {
272: return button;
273: }
274: LOG.debug("Looking for \"normal\" button in " + form);
275: return findNormalButton(form);
276: }
277:
278: private ClickableElement findButtonAllForms(
279: final HtmlPage currentResp) {
280: LOG
281: .debug("Looking for button in all forms contained in the document");
282: List forms = currentResp.getForms();
283: if (forms.size() == 0) {
284: LOG
285: .warn("No forms found - page probably non-compliant - searching page anyway");
286: return searchButton(currentResp);
287: }
288: for (final Iterator iter = forms.iterator(); iter.hasNext();) {
289: ClickableElement button = findButton((HtmlForm) iter.next());
290: if (button != null) {
291: return button;
292: }
293: }
294: return null;
295: }
296:
297: private static boolean isButton(HtmlElement elt) {
298: if (elt instanceof HtmlButton) {
299: LOG.debug("It's a button, that's ok");
300: return true;
301: }
302: if (elt instanceof HtmlInput
303: && isInputButtonType((HtmlInput) elt)) {
304: LOG.debug("It's an "
305: + elt.getAttributeValue(HtmlConstants.TYPE)
306: + " input, that's ok");
307: return true;
308: }
309: LOG.debug("Html element is not a button");
310: return false;
311: }
312:
313: private static boolean isInputButtonType(final HtmlInput input) {
314: return INPUT_BUTTONS_TYPES.contains(input.getTypeAttribute()
315: .toLowerCase());
316: }
317:
318: private ClickableElement findInputButton(final HtmlForm form) {
319: final Collection inputButtons = form
320: .getHtmlElementsByTagName(HtmlConstants.INPUT);
321: return findInputButton(inputButtons.iterator());
322: }
323:
324: private ClickableElement searchButton(final HtmlPage page) {
325: final Collection buttons = new ArrayList();
326: final Iterator childElements = page.getAllHtmlChildElements();
327: while (childElements.hasNext()) {
328: final HtmlElement elt = (HtmlElement) childElements.next();
329: if (isButton(elt)) {
330: buttons.add(elt);
331: }
332: }
333: ClickableElement button = findInputButton(buttons.iterator());
334: if (button == null) {
335: button = findNormalButton(buttons.iterator());
336: }
337: return button;
338: }
339:
340: private ClickableElement findInputButton(
341: final Iterator candidateIterator) {
342: int indexFound = 0; // should index be across both button types? currently not
343: while (candidateIterator.hasNext()) {
344: final HtmlElement curElement = (HtmlElement) candidateIterator
345: .next();
346: if (!(curElement instanceof HtmlInput)) {
347: continue;
348: }
349: final HtmlInput curInput = (HtmlInput) curElement;
350: if (!isInputButtonType(curInput)) {
351: continue; // not a "button"
352: }
353: LOG.debug("Examining button: " + curInput);
354: if (hasMatchingNameOrDontCare(curInput)
355: && hasMatchingLabelOrDontCare(curInput)) {
356: if (indexFound == ConversionUtil.convertToInt(
357: getFieldIndex(), 0)) {
358: LOG.debug(curInput.getTypeAttribute()
359: + " button found: " + curInput);
360: return curInput;
361: }
362: ++indexFound;
363: }
364: }
365: return null;
366: }
367:
368: private ClickableElement findNormalButton(final HtmlForm form) {
369: return findNormalButton(form.getAllHtmlChildElements());
370: }
371:
372: private ClickableElement findNormalButton(
373: final Iterator candidateIterator) {
374: int indexFound = 0; // should index be across both button types? currently not
375: while (candidateIterator.hasNext()) {
376: final HtmlElement curElement = (HtmlElement) candidateIterator
377: .next();
378: if (!(curElement instanceof HtmlButton)) {
379: continue;
380: }
381: final HtmlButton curButton = (HtmlButton) curElement;
382: LOG.debug("Examining button: " + curButton);
383: if (hasMatchingNameOrDontCare(curButton)
384: && hasMatchingLabelOrDontCare(curButton)) {
385: if (indexFound == ConversionUtil.convertToInt(
386: getFieldIndex(), 0)) {
387: LOG.debug("Normal button found: " + curButton);
388: return curButton;
389: }
390: ++indexFound;
391: }
392: }
393: return null;
394: }
395:
396: private boolean hasMatchingNameOrDontCare(
397: final HtmlElement curButton) {
398: if (curButton instanceof HtmlInput) {
399: return hasMatchingNameOrDontCare((HtmlInput) curButton);
400: }
401: if (curButton instanceof HtmlButton) {
402: return hasMatchingNameOrDontCare((HtmlButton) curButton);
403: }
404: throw new IllegalArgumentException(
405: "Button is neither a HtmlInput nor a HtmlButton: "
406: + curButton);
407: }
408:
409: private boolean hasMatchingNameOrDontCare(final HtmlInput curButton) {
410: return getName() == null
411: || getName().equals(curButton.getNameAttribute());
412: }
413:
414: private boolean hasMatchingNameOrDontCare(final HtmlButton curButton) {
415: return getName() == null
416: || getName().equals(curButton.getNameAttribute());
417: }
418:
419: private boolean hasMatchingLabelOrDontCare(
420: final HtmlElement curButton) {
421: if (getLabel() == null)
422: return true;
423: else if (curButton instanceof HtmlImageInput) {
424: return getLabel().equals(
425: ((HtmlImageInput) curButton).getAltAttribute());
426: } else {
427: return getLabel().equals(curButton.asText());
428: }
429: }
430:
431: protected boolean isImageButton() {
432: return !StringUtils.isEmpty(getX())
433: && !StringUtils.isEmpty(getY());
434: }
435:
436: /**
437: * Called by Ant to set the text nested between opening and closing tags.
438: * @param text the text to set
439: * @webtest.nested.parameter
440: * required="no"
441: * description="Alternative way to set the 'label' attribute."
442: */
443: public void addText(final String text) {
444: setLabel(getProject().replaceProperties(text));
445: }
446: }
|