com.xceptance.xlt.api.actions.AbstractHtmlPageAction Maven / Gradle / Ivy
Show all versions of xlt Show documentation
/*
* Copyright (c) 2005-2022 Xceptance Software Technologies GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xceptance.xlt.api.actions;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlHiddenInput;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlSelect;
import com.gargoylesoftware.htmlunit.html.SubmittableElement;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import com.xceptance.common.util.ParameterCheckUtils;
import com.xceptance.xlt.api.engine.NetworkData;
import com.xceptance.xlt.api.engine.NetworkDataManager;
import com.xceptance.xlt.api.engine.Session;
import com.xceptance.xlt.api.util.XltException;
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.common.XltConstants;
import com.xceptance.xlt.engine.SessionImpl;
import com.xceptance.xlt.engine.XltWebClient;
import com.xceptance.xlt.engine.resultbrowser.RequestHistory;
/**
* AbstractHtmlPageAction is the base class for all HTML-based actions. In contrast to
* {@link AbstractLightWeightPageAction}, the loaded page is parsed and stored internally as a tree of elements. This
* makes it easy to locate and query/manipulate a certain page element.
*
* In case JavaScript processing is enabled, the loaded pages might start JS background jobs (via window.setTimeout()
* and window.setInterval()). In order to wait for these jobs to finish, an optional waiting time can be specified for
* each of the page loading methods. The framework will not return from these methods unless either all background jobs
* are finished or the specified waiting time is exceeded. Note that in the latter case any pending (i.e. scheduled, but
* not running yet) background job is removed, while any running job is left running. If no waiting time is specified, a
* default waiting time is read from the configuration.
*
* Note that if the specified waiting time is negative, the framework will not wait for any job to finish, nor will it
* cancel pending jobs.
*
* @see AbstractLightWeightPageAction
* @author Jörg Werner (Xceptance Software Technologies GmbH)
*/
public abstract class AbstractHtmlPageAction extends AbstractWebAction
{
/**
* Waiting time property key.
*/
private static final String PROP_JS_BACKGROUND_ACTIVITY_WAITINGTIME = XltConstants.XLT_PACKAGE_PATH +
".js.backgroundActivity.waitingTime";
/**
* Default waiting time value.
*/
private static final long DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME = XltProperties.getInstance()
.getProperty(PROP_JS_BACKGROUND_ACTIVITY_WAITINGTIME,
-1);
/**
* The parsed HTML page object generated by this action.
*/
private HtmlPage htmlPage;
/**
* Creates a new AbstractHtmlPageAction object and gives it the passed timer name. This constructor is typically
* used for an intermediate action in a sequence of actions, i.e. it has a previous action.
*
* @param previousAction
* the action that preceded the current action
* @param timerName
* the name of the timer that is associated with this action
*/
protected AbstractHtmlPageAction(final AbstractWebAction previousAction, final String timerName)
{
super(previousAction, timerName);
}
/**
* Creates a new AbstractHtmlPageAction object and gives it the passed timer name. This constructor is typically
* used for the first action in a sequence of actions, i.e. it has no previous action.
*
* @param timerName
* the name of the timer that is associated with this action
*/
protected AbstractHtmlPageAction(final String timerName)
{
this(null, timerName);
}
/**
* Returns the parsed HTML page object generated by this action.
*
* @return the page
*/
public HtmlPage getHtmlPage()
{
return htmlPage;
}
/**
* {@inheritDoc}
*/
@Override
public AbstractHtmlPageAction getPreviousAction()
{
return (AbstractHtmlPageAction) super.getPreviousAction();
}
/**
* {@inheritDoc}
*/
@Override
public void run() throws Throwable
{
try
{
super.run();
}
finally
{
/*
* Dump the page not before the very end of this action. This way, all requests that are executed after one
* of the loadPageByXXX() methods are correctly associated with this action.
*/
dumpPage(getHtmlPage());
Session.getCurrent().getNetworkDataManager().clear();
}
}
/**
* Sets the given HTML page object to be the result of this action. Typically, the HTML page is generated by one of
* the loadPage...()
methods and need not be set explicitly.
*
* @param htmlPage
* the page to set
*/
public void setHtmlPage(final HtmlPage htmlPage)
{
setHtmlPage(htmlPage, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Sets the given HTML page object to be the result of this action and waits for background task to be finished.
* Typically, the HTML page is generated by one of the loadPage...()
methods and need not be set
* explicitly.
*
* @param htmlPage
* the page to set
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
public void setHtmlPage(final HtmlPage htmlPage, final long waitingTime)
{
ParameterCheckUtils.isNotNull(htmlPage, "htmlPage");
this.htmlPage = waitForPageIsComplete(htmlPage, waitingTime);
}
/**
* Loads the page using the given request method from the passed URL. The specified request parameters are added
* either to the request body if the request method is {@link HttpMethod#POST} or appended to the query string of
* the target URL for any other request method.
*
* @param url
* the target URL
* @param method
* the HTTP request method to be used
* @param requestParameters
* the list of custom parameters to add
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final URL url, final HttpMethod method, final List requestParameters, final long waitingTime)
throws Exception
{
final WebRequest webRequest = createWebRequestSettings(url, method, requestParameters);
final Page result = getWebClient().getPage(webRequest);
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page using the given request method from the passed URL. The specified request parameters are added
* either to the request body if the request method is {@link HttpMethod#POST} or appended to the query string of
* the target URL for any other request method.
*
* @param url
* the target URL
* @param method
* the HTTP request method to be used
* @param requestParameters
* the list of custom parameters to add
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final URL url, final HttpMethod method, final List requestParameters) throws Exception
{
loadPage(url, method, requestParameters, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page using HTTP GET from the passed URL and waits for background task to be finished.
*
* @param url
* the target URL
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final URL url, final long waitingTime) throws Exception
{
loadPage(url, HttpMethod.GET, EMPTY_PARAMETER_LIST, waitingTime);
}
/**
* Loads the page using HTTP GET from the passed URL.
*
* @param url
* the target URL
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final URL url) throws Exception
{
loadPage(url, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page using HTTP GET from the passed URL.
*
* @param urlAsString
* the target URL as string
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final String urlAsString, final long waitingTime) throws Exception
{
final URL url = new URL(urlAsString);
loadPage(url, waitingTime);
}
/**
* Loads the page using HTTP GET from the passed URL.
*
* @param urlAsString
* the target URL as string
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPage(final String urlAsString) throws Exception
{
loadPage(urlAsString, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by "clicking" the passed HTML element.
*
* @param element
* the HTML element to click
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByClick(final HtmlElement element, final long waitingTime) throws Exception
{
final Page result = element.click();
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by "clicking" the passed HTML element.
*
* @param element
* the HTML element to click
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByClick(final HtmlElement element) throws Exception
{
loadPageByClick(element, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by drag and drop
*
* @param draggable
* the dragged object
* @param dropTarget
* the element on which to drop
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByDragAndDrop(final HtmlElement draggable, final HtmlElement dropTarget, final long waitingTime) throws Exception
{
draggable.mouseDown();
dropTarget.mouseMove();
final Page result = dropTarget.mouseUp();
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by drag and drop
*
* @param draggable
* the dragged object
* @param dropTarget
* the element on which to drop
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByDragAndDrop(final HtmlElement draggable, final HtmlElement dropTarget) throws Exception
{
loadPageByDragAndDrop(draggable, dropTarget, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by "clicking" the passed HTML element in the specified HTML form element. This method skips hidden
* elements and reports an error if a suitable element to click on was not found.
*
* @param form
* the HTML form
* @param elementToClick
* the HTML element to click, cannot be a hidden element, this can be the name or the id. The name will
* be tried first, after that the id.
* @param randomPosition
* (parameter ignored)
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
* @throws ElementMissingException
* if we do not have an element to click on
* @deprecated As of XLT 4.9.0, use {@link #loadPageByFormClick(HtmlForm, String, long)} instead.
*/
@Deprecated
protected void loadPageByFormClick(final HtmlForm form, final String elementToClick, final boolean randomPosition,
final long waitingTime)
throws Exception
{
loadPageByFormClick(form, elementToClick, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by "clicking" the passed HTML element in the specified HTML form element. This method skips hidden
* elements and reports an error if a suitable element to click on was not found.
*
* @param form
* the HTML form
* @param elementToClick
* the HTML element to click, cannot be a hidden element, this can be the name or the id. The name will
* be tried first, after that the id.
* @param randomPosition
* (parameter ignored)
* @throws Exception
* if an error occurred while loading the page
* @throws ElementMissingException
* if we do not have an element to click on
* @deprecated As of XLT 4.9.0, use {@link #loadPageByFormClick(HtmlForm, String)} instead.
*/
@Deprecated
protected void loadPageByFormClick(final HtmlForm form, final String elementToClick, final boolean randomPosition) throws Exception
{
loadPageByFormClick(form, elementToClick);
}
/**
* Loads the page by "clicking" the passed HTML element in the specified HTML form element.
*
* @param form
* the HTML form
* @param elementToClick
* the HTML element to click, cannot be a hidden element, this can be the name or the id. The name will
* be tried first, after that the id.
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByFormClick(final HtmlForm form, final String elementToClick, final long waitingTime) throws Exception
{
final List elements = new ArrayList();
// the input elements first
elements.addAll(form.getInputsByName(elementToClick));
// the form buttons second
elements.addAll(form.getButtonsByName(elementToClick));
// in case we do not have at least an element by name - try the ID
if (elements.isEmpty())
{
elements.add(((HtmlPage) form.getPage()).getHtmlElementById(elementToClick));
}
// ok, we might have more than one element with the same name, not nice
// but possible
// to avoid picking the wrong one, we at least skip elements that are
// hidden
HtmlElement element2Click = null;
for (int i = 0; i < elements.size(); i++)
{
final HtmlElement input = elements.get(i);
if (!(input instanceof HtmlHiddenInput))
{
element2Click = input;
break;
}
}
// we cannot execute this load operation
if (element2Click == null)
{
throw new ElementMissingException("No element with name '" + elementToClick + "' found that can be clicked on.");
}
Page result = element2Click.click();
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by "clicking" the passed HTML element in the specified HTML form element.
*
* @param form
* the HTML form
* @param elementToClick
* the HTML element to click
* @throws Exception
* if an error occurred while loading the page
*/
protected void loadPageByFormClick(final HtmlForm form, final String elementToClick) throws Exception
{
loadPageByFormClick(form, elementToClick, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by selecting an option of the given HTML select element.
*
* @param select
* the HTML select element
* @param optionValue
* the value of the HTML option to select
*/
protected void loadPageBySelect(final HtmlSelect select, final String optionValue) throws Exception
{
loadPageBySelect(select, optionValue, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by selecting an option from the given HTML select element.
*
* @param select
* the HTML select
* @param optionValue
* the value of the HTML option to select
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
protected void loadPageBySelect(final HtmlSelect select, final String optionValue, final long waitingTime) throws Exception
{
final Page result = select.setSelectedAttribute(optionValue, true);
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by selecting an option form the given HTML select element.
*
* @param select
* the HTML select
* @param option
* the HTML option to select
*/
protected void loadPageBySelect(final HtmlSelect select, final HtmlOption option)
{
final Page result = select.setSelectedAttribute(option, true);
htmlPage = waitForPageIsComplete(result, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by selecting an option from the given HTML select element.
*
* @param select
* the HTML select
* @param option
* the HTML option to select
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
protected void loadPageBySelect(final HtmlSelect select, final HtmlOption option, final long waitingTime) throws Exception
{
final Page result = select.setSelectedAttribute(option, true);
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by submitting the given form. Form submission is triggered by the given submittable element.
*
* @param form
* the HTML form to submit
* @param element
* the submittable element used to trigger form submission
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
protected void loadPageByFormSubmit(final HtmlForm form, final SubmittableElement element, final long waitingTime) throws Exception
{
// Note that HtmlForm.submit() is not public any longer.
// htmlPage = waitForPageIsComplete(form.submit(element), waitingTime);
// HACK: Use reflection to call the method anyway.
final Method submitMethod = HtmlForm.class.getDeclaredMethod("submit", SubmittableElement.class);
submitMethod.setAccessible(true);
try
{
submitMethod.invoke(form, element);
final WebWindow w = getWebClient().getCurrentWindow();
// note, that processing postponed actions should normally play no role when javascript
// is disabled, but HtmlUnit doesn't really care about that so this code should work well
// for both cases: javascript on and javascript off
getWebClient().getJavaScriptEngine().processPostponedActions();
htmlPage = waitForPageIsComplete(w.getEnclosedPage(), waitingTime);
}
catch (final InvocationTargetException te)
{
final Throwable cause = te.getCause();
if (cause instanceof Exception)
{
throw (Exception) cause;
}
throw new XltException("Failed to submit form", cause);
}
}
/**
* Loads the page by submitting the given form. Form submission is triggered by the given submittable element.
*
* @param form
* the HTML form to submit
* @param element
* the submittable element used to trigger form submission
*/
protected void loadPageByFormSubmit(final HtmlForm form, final SubmittableElement element) throws Exception
{
loadPageByFormSubmit(form, element, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by submitting the given form.
*
* @param form
* the HTML form to submit
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
protected void loadPageByFormSubmit(final HtmlForm form, final long waitingTime) throws Exception
{
loadPageByFormSubmit(form, null, waitingTime);
}
/**
* Loads the page by submitting the given form.
*
* @param form
* the HTML form to submit
*/
protected void loadPageByFormSubmit(final HtmlForm form) throws Exception
{
loadPageByFormSubmit(form, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Loads the page by typing keys. Whether or not the page is reloaded depends on the key event handlers attached to
* the target HTML element.
*
* @param element
* the target HTML element
* @param text
* the text to type
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
*/
protected void loadPageByTypingKeys(final HtmlElement element, final String text, final long waitingTime) throws Exception
{
Page result = element.getPage();
for (final char ch : text.toCharArray())
{
result = element.type(ch);
}
htmlPage = waitForPageIsComplete(result, waitingTime);
}
/**
* Loads the page by typing keys. Whether or not the page is reloaded depends on the key event handlers attached to
* the target HTML element.
*
* @param element
* the target HTML element
* @param text
* the text to type
*/
protected void loadPageByTypingKeys(final HtmlElement element, final String text) throws Exception
{
loadPageByTypingKeys(element, text, DEFAULT_JS_BACKGROUND_ACTIVITY_WAITINGTIME);
}
/**
* Waits at most for the given waiting time to let any background JavaScript activity on the specified page
* complete. The resulting page is scanned for any new references to static content, which will be downloaded as
* well.
*
* @param page
* the page
* @param waitingTime
* Waiting time for all background tasks on the page to be finished
* @return the possibly modified page
*/
private HtmlPage waitForPageIsComplete(final Page page, final long waitingTime)
{
// wait for any JavaScript background thread to finish
if (page instanceof SgmlPage)
{
final XltWebClient webClient = (XltWebClient) ((SgmlPage) page).getWebClient();
webClient.waitForBackgroundThreads(page.getEnclosingWindow().getTopWindow().getEnclosedPage(), waitingTime);
}
// something might have changed, including a reload via location
Page enclosedPage = page.getEnclosingWindow().getTopWindow().getEnclosedPage();
final HtmlPage newHtmlPage;
// check whether the server indeed returned HTML content
if (enclosedPage instanceof HtmlPage)
{
// yes
newHtmlPage = (HtmlPage) enclosedPage;
// check for any new static content to load
((XltWebClient) newHtmlPage.getWebClient()).loadNewStaticContent(newHtmlPage);
}
else
{
// no, the server returned unexpected content (e.g. plain text in a 404 message)
throw new XltException("The server response could not be parsed as HTML.");
}
// Feature #471: API: Make the network data available for validation
collectAndSetNetworkData();
return newHtmlPage;
}
/**
* Dumps the given HTML page object to the request history.
*
* @param htmlPage
* the page to dump
*/
private void dumpPage(final HtmlPage htmlPage)
{
final String timerName = ((XltWebClient) getWebClient()).getTimerName();
final RequestHistory requestHistory = SessionImpl.getCurrent().getRequestHistory();
if (htmlPage != null)
{
requestHistory.add(timerName, htmlPage);
}
else
{
// dump at least an empty page to let the action appear in the result browser
requestHistory.add(timerName);
}
}
//
// === Feature #471: API: Make the network data available for validation ===
//
/**
* Network data set.
*/
private List netStats = null;
/**
* Gets the network data from the {@link NetworkDataManager} and sets the appropriate field.
*/
private void collectAndSetNetworkData()
{
netStats = Session.getCurrent().getNetworkDataManager().getData();
}
/**
* Returns the network data set.
*
* @return network data as set
*/
protected List getNetworkDataSet()
{
return netStats;
}
}