/* * 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 } }