/*
* JBoss, Home of Professional Open Source
* Copyright 2010-2016, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.tests.metamer.ftest.webdriver;
import static java.lang.String.format;
import static org.testng.Assert.assertEquals;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import javax.faces.event.PhaseId;
import org.apache.commons.lang.ArrayUtils;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.Graphene;
import org.jboss.arquillian.graphene.context.GrapheneContext;
import org.jboss.arquillian.graphene.proxy.GrapheneProxy;
import org.jboss.arquillian.graphene.proxy.GrapheneProxyInstance;
import org.jboss.arquillian.graphene.proxy.Interceptor;
import org.jboss.arquillian.graphene.proxy.InvocationContext;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.richfaces.fragment.status.RichFacesStatus;
/**
* @author <a href="mailto:jstefek@redhat.com">Jiri Stefek</a>
* @author <a href="https://community.jboss.org/people/ppitonak">Pavol Pitonak</a>
*/
public class MetamerPage {
protected static final int MINOR_WAIT_TIME = 50; // ms
public static final String STRING_ACTIONLISTENER_MSG = "action listener invoked";
public static final String STRING_ACTION_MSG = "action invoked";
public static final String STRING_EXECUTE_CHECKER_MSG = "executeChecker";
protected static final int TRIES = 20;// for guardListSize and expectedReturnJS
/**
* root element for component attributes area
*/
@FindBy(css = "table[id$='attributes:attributes']")
private WebElement attributesTable;
@FindBy(id = "blurButtonFooter")
private WebElement blurButton;
@Drone
protected WebDriver driver;
@ArquillianResource
private JavascriptExecutor executor;
@FindBy(css = "[id$=fullPageRefreshImage]")
private WebElement fullPageRefreshIcon;
@FindBy(css = "span[id$=jsFunctionChecker]")
private WebElement jsFunctionChecker;
private Map<PhaseId, List<String>> map = new LinkedHashMap<PhaseId, List<String>>();
@FindBy(css = "div[id$=phasesPanel] li")
private List<WebElement> phases;
@FindBy(css = "span[id$=renderChecker]")
private WebElement renderCheckerOutput;
private String reqTime;
@FindBy(css = "span[id$=requestTime]")
private WebElement requestTime;
@FindBy(css = "[id$=reRenderAllImage]")
private WebElement rerenderAllIcon;
/**
* Delay response by [ms]
*/
@FindBy(css = "input[id$='metamerResponseDelayInput']")
private WebElement responseDelay;
@FindBy(css = "[id$='a4jStatusPanel']")
private RichFacesStatus status;
@FindBy(css = "span[id$=statusCheckerOutput]")
private WebElement statusCheckerOutput;
/**
* Method for guarding that Metamer's requestTime changes.
*
* @param <T> type of the given target
* @param target object to be guarded
* @return guarded element
*/
public static <T> T requestTimeChangesWaiting(T target) {
return requestTimeWaiting(target, new RequestTimeChangesWaitingInterceptor());
}
/**
* Method for guarding that Metamer's requestTime not changes. Guards for 1 second.
*
* @param <T> type of the given target
* @param target object to be guarded
* @return guarded element
*/
public static <T> T requestTimeNotChangesWaiting(T target) {
return requestTimeNotChangesWaiting(target, 1000);
}
/**
* Method for guarding that Metamer's requestTime not changes.
*
* @param <T> type of the given target
* @param target object to be guarded
* @param waitTimeInMillis time for which will be the target guarded
* @return guarded element
*/
public static <T> T requestTimeNotChangesWaiting(T target, long waitTimeInMillis) {
return requestTimeWaiting(target, new RequestTimeNotChangesWaitingInterceptor(waitTimeInMillis));
}
private static <T> T requestTimeWaiting(T target, Interceptor interceptor) {
GrapheneProxyInstance proxy;
if (GrapheneProxy.isProxyInstance(target)) {
proxy = (GrapheneProxyInstance) ((GrapheneProxyInstance) target).copy();
} else {
throw new IllegalStateException("Can't create a new proxy.");
}
proxy.registerInterceptor(interceptor);
return (T) proxy;
}
/**
* !All requests depends on Metamer`s requestTime! Generates a waiting proxy. The proxy will wait for expected
*
* @waitRequestType which will be launched via interactions with @target and then it waits until Metamer's request time
* changes(@waitRequestType is HTTP or XHR) or not changes(@waitRequestType is NONE).
*
* @param <T> type of the given target
* @param target object to be guarded
* @param waitRequestType type of expected request which will be launched
* @return waiting proxy for target object
*/
public static <T> T waitRequest(T target, WaitRequestType waitRequestType) {
switch (waitRequestType) {
case HTTP:
return requestTimeChangesWaiting(Graphene.guardHttp(target));
case XHR:
return requestTimeChangesWaiting(Graphene.guardAjax(target));
case NONE:
return requestTimeNotChangesWaiting(Graphene.guardNoRequest(target));
default:
throw new UnsupportedOperationException("Not supported request: " + waitRequestType);
}
}
/**
* !All requests depends on Metamer`s requestTime! Generates a waiting proxy. The proxy will wait for expected
*
* @waitRequestType which will be launched via interactions with @target and then it waits until Metamer's request time
* changes(@waitRequestType is HTTP or XHR) or not changes (@waitRequestType is NONE).
*
* @param <T> type of the given target
* @param target object to be guarded
* @param waitRequestType type of expected request which will be launched
* @param guardTime time for which will be the target guarded, applicable only for @waitRequestType = NONE
* @return waiting proxy for target object
*/
public static <T> T waitRequest(T target, WaitRequestType waitRequestType, long guardTime) {
switch (waitRequestType) {
case HTTP:
return requestTimeChangesWaiting(Graphene.guardHttp(target));
case XHR:
return requestTimeChangesWaiting(Graphene.guardAjax(target));
case NONE:
return requestTimeNotChangesWaiting(Graphene.guardNoRequest(target), guardTime);
default:
throw new UnsupportedOperationException("Not supported request: " + waitRequestType);
}
}
/**
* Waiting method. Waits number of milis defined by @milis
*
* @param milis
*/
public static void waiting(long milis) {
try {
Thread.sleep(milis);
} catch (InterruptedException ignored) {
}
}
/**
* Method for checking bypass updates JSF phases cycle for a4j:commandButton and a4j:commandLink. Asserts that phases
* contains phases: RESTORE_VIEW, APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, RENDER_RESPONSE.
*/
public void assertBypassUpdatesPhasesCycle() {
initialize();
assertPhases(PhaseId.RESTORE_VIEW, PhaseId.APPLY_REQUEST_VALUES, PhaseId.PROCESS_VALIDATIONS,
PhaseId.RENDER_RESPONSE);
}
/**
* Method for checking immediate JSF phases cycle for a4j:commandButton and a4j:commandLink. Asserts that phases contains
* phases: RESTORE_VIEW, APPLY_REQUEST_VALUES, RENDER_RESPONSE.
*/
public void assertImmediatePhasesCycle() {
initialize();
assertPhases(PhaseId.RESTORE_VIEW, PhaseId.APPLY_REQUEST_VALUES, PhaseId.RENDER_RESPONSE);
}
/**
* Asserts that the listener occurred in given phase for a number of times.
*
* @param inPhase the phase where the listener occurred
* @param listenerMessage the part of the message which it should be looked up
* @param expectedInvocationCount number of the same messages which should be looked up
*/
public void assertListener(PhaseId inPhase, String listenerMessage, int expectedInvocationCount) {
initialize();
List<String> list = map.get(inPhase);
String messageWithoutStar = listenerMessage.startsWith("*") ? listenerMessage.substring(2) : listenerMessage;
int invocationCount = 0;
if (list != null && list.size() > 0) {
for (String description : list) {
if (description.contains(messageWithoutStar)) {
invocationCount++;
}
}
}
assertEquals(invocationCount, expectedInvocationCount,
format("The count of listener's invocations does not match. Expected <%s> invocation(s), but had: <%s> invocation(s).", expectedInvocationCount, invocationCount));
}
/**
* Asserts that the listener occurred in given phase only once.
*
* @param phaseId the phase where the listener occurred
* @param message the part of the message which it should be looked up
*/
public void assertListener(PhaseId phaseId, String message) {
assertListener(phaseId, message, 1);
}
/**
* Asserts that there is no specified message in phases list.
*
* @param message the part of the message which it should be looked up
*/
public void assertNoListener(String message) {
initialize();
String messageWithoutStar = message.startsWith("*") ? message.substring(2) : message;
for (Entry<PhaseId, List<String>> entry : map.entrySet()) {
PhaseId phaseId = entry.getKey();
List<String> descriptions = entry.getValue();
for (String description : descriptions) {
if (description.contains(messageWithoutStar)) {
throw new AssertionError("The '" + messageWithoutStar + "' was found across messages in phase " + phaseId);
}
}
}
}
/**
* Asserts that the phases has occurred in last request by the specified list.
*/
public void assertPhases(PhaseId... expectedPhases) {
initialize();
if (ArrayUtils.contains(expectedPhases, PhaseId.ANY_PHASE)) {
expectedPhases = new LinkedList<PhaseId>(PhaseId.VALUES).subList(1, 7).toArray(new PhaseId[6]);
}
PhaseId[] actualPhases = map.keySet().toArray(new PhaseId[map.size()]);
assertEquals(actualPhases, expectedPhases);
}
public void blur(WaitRequestType g) {
WebElement button = getBlurButton();
switch (g) {
case XHR:
button = Graphene.guardAjax(button);
break;
case HTTP:
button = Graphene.guardHttp(button);
break;
case NONE:
break;
default:
throw new UnsupportedOperationException("unknown switch: " + g);
}
button.click();
}
/**
* Executes JavaScript script. Method will execute the script few times until an expected String is returned, the String is
* defined in @expectedValue. Returns a single trimmed String with expected value or what it has found or null.
*
* @param expectedValue expected return value of javaScript
* @param script whole JavaScript that will be executed
* @param args
* @return single and trimmed string or null
*/
protected String expectedReturnJS(String script, String expectedValue, Object... args) {
JavascriptExecutor js = (JavascriptExecutor) driver;
String result = null;
for (int i = 0; i < TRIES; i++) {
Object executedScriptResult = js.executeScript(script, args);
if (executedScriptResult != null) {
result = ((String) executedScriptResult).trim();
if (result.equals(expectedValue)) {
break;
}
}
}
return result;
}
/**
* Do a full page refresh (regular HTTP request) by triggering a command with no action bound.
*/
public void fullPageRefresh() {
performJSClickOnButton(fullPageRefreshIcon, WaitRequestType.HTTP);
}
public WebElement getAttributesTableElement() {
return attributesTable;
}
public WebElement getBlurButton() {
return blurButton;
}
public WebElement getJsFunctionCheckerElement() {
return jsFunctionChecker;
}
private PhaseId getPhaseId(String phaseIdentifier) {
for (PhaseId phaseId : PhaseId.VALUES) {
if (phaseIdentifier.startsWith(phaseId.toString())) {
return phaseId;
}
}
throw new IllegalStateException("no such phase '" + phaseIdentifier + "'");
}
public List<String> getPhases() {
List<String> result = new ArrayList<String>();
for (WebElement webElement : phases) {
result.add(webElement.getText());
}
return result;
}
public List<WebElement> getPhasesElements() {
return phases;
}
public WebElement getRenderCheckerOutputElement() {
return renderCheckerOutput;
}
public WebElement getRequestTimeElement() {
return requestTime;
}
public WebElement getResponseDelayElement() {
return responseDelay;
}
public RichFacesStatus getStatus() {
return status;
}
public WebElement getStatusCheckerOutputElement() {
return statusCheckerOutput;
}
private void initialize() {
if (reqTime == null || !reqTime.equals(requestTime.getText())) {
reqTime = requestTime.getText();
map.clear();
List<String> list = null;
for (WebElement element : phases) {
String description = element.getText();
if (!description.startsWith("*")) {
list = new LinkedList<String>();
map.put(getPhaseId(description), list);
} else {
list.add(description.substring(2));
}
}
}
}
/**
* The button can be hidden by the popup panel and so it cannot be clicked. This method workarounds the problem without a
* need of moving the panel.
*/
public void performJSClickOnButton(WebElement button, WaitRequestType type) {
JavascriptExecutor e = executor;
switch (type) {
case HTTP:
e = Graphene.guardHttp(e);
break;
case XHR:
e = Graphene.guardAjax(e);
break;
case NONE:
break;
default:
throw new UnsupportedOperationException();
}
e.executeScript("arguments[0].click()", button);
}
/**
* Rerender all content of the page (AJAX request) by trigerring a command with no action but render bound.
*/
public void rerenderAll() {
performJSClickOnButton(rerenderAllIcon, WaitRequestType.XHR);
}
private static class RequestTimeChangesWaitingInterceptor implements Interceptor {
private static final By REQUEST_TIME = By.cssSelector("span[id$='requestTime']");
protected String time1;
protected void afterAction(GrapheneContext context) {
Graphene.waitModel(context.getWebDriver()).until().element(REQUEST_TIME).text().not().equalTo(time1);
}
protected void beforeAction(GrapheneContext context) {
time1 = getTime(context.getWebDriver());
}
@Override
public int getPrecedence() {
return 1;
}
protected String getTime(SearchContext searchContext) {
String time = searchContext.findElement(REQUEST_TIME).getText();
return time;
}
@Override
public Object intercept(InvocationContext context) throws Throwable {
beforeAction(context.getGrapheneContext());
Object o = context.invoke();
afterAction(context.getGrapheneContext());
return o;
}
}
private static class RequestTimeNotChangesWaitingInterceptor extends RequestTimeChangesWaitingInterceptor {
private final long guardTime;// ms
public RequestTimeNotChangesWaitingInterceptor(long waitTime) {
this.guardTime = waitTime;
}
@Override
protected void afterAction(GrapheneContext context) {
waiting(guardTime);
if (!getTime(context.getWebDriver()).equals(time1)) {
throw new RuntimeException("No request expected, but request time has changed.");
}
}
}
/**
* WebDriver wait which ignores StaleElementException and NoSuchElementException and is polling every 50 ms.
*/
public static class WDWait extends WebDriverWait {
/**
* WebDriver wait which ignores StaleElementException and NoSuchElementException and polling every 50 ms with max wait
* time of 5 seconds
*/
public WDWait(WebDriver browser) {
this(browser, 5);
}
/**
* WebDriver wait which ignores StaleElementException and NoSuchElementException and polling every 50 ms with max wait
* time set in attribute
*
* @param seconds max wait time
*/
public WDWait(WebDriver browser, int seconds) {
super(browser, seconds);
ignoring(NoSuchElementException.class);
ignoring(StaleElementReferenceException.class);
pollingEvery(50, TimeUnit.MILLISECONDS);
}
}
public enum WaitRequestType {
HTTP, NONE, XHR
}
}