package org.jboss.seam.test.unit; import org.jboss.seam.Component; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.intercept.BypassInterceptors; import org.jboss.seam.contexts.Context; import org.jboss.seam.contexts.Contexts; import org.jboss.seam.faces.FacesManager; import org.jboss.seam.mock.MockHttpServletRequest; import org.jboss.seam.navigation.Pages; import org.jboss.seam.test.unit.component.TestActions; import org.testng.annotations.Test; import javax.faces.context.FacesContext; import javax.faces.render.ResponseStateManager; import java.util.List; import java.util.Map; /** * The purpose of this test is to verify the way that page actions are handled. Once * a page action triggers a navigation event, subsequent page actions in the chain * should be short circuited. */ public class PageActionsTest extends AbstractPageTest { @Override protected void installComponents(Context appContext) { super.installComponents(appContext); installComponent(appContext, NoRedirectFacesManager.class); installComponent(appContext, TestActions.class); } /** * This test verifies that a non-null outcome will short-circuit the page * actions. It tests two difference variations. The first variation includes * both actions as nested elements of the page node. The second variation has * the first action in the action attribute of the page node and the second * action as a nested element. Aside from the placement of the actions, the * two parts of the test are equivalent. */ @Test(enabled = true) public void testShortCircuitOnNonNullOutcome() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test01a.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/pageA.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA" }); testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test01b.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/pageA.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA" }); } /** * This test verifies that because the first action does not result in a * navigation, the second action is executed. However, the third action is * not called because of the navigation on the second action. */ @Test(enabled = true) public void testShortCircuitInMiddle() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test02.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/pageB.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA", "nonNullActionB" }); } /** * This test verifies that an action method with a null return can still match * a navigation rule and short-circuit the remaining actions. The key is that * a navigation rule is matched, not what the return value is. */ @Test(enabled = true) public void testShortCircuitOnNullOutcome() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test03.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/pageA.xhtml"); assertActionCalls(testActions, new String[] { "nullActionA" }); } /** * Verify that the first non-null outcome, even if it is to the same view id, * will short circuit the action calls. */ @Test(enabled = true) public void testShortCircuitOnNonNullOutcomeToSamePage() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test04.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/action-test04.xhtml"); assertActionCalls(testActions, new String[] { "nullActionA", "nonNullActionB" }); } /** * Same as testShortCircuitOnNonNullOutcome except that the navigation rules * are redirects rather than renders. */ @Test(enabled = true) public void testShortCircuitOnNonNullOutcomeWithRedirect() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test05.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/action-test05.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA" }); assert Contexts.getEventContext().get("lastRedirectViewId").equals("/pageA.xhtml") : "Expecting a redirect to /pageA.xhtml but redirected to " + Contexts.getEventContext().get("lastRedirectViewId"); assert facesContext.getResponseComplete() == true : "The response should have been marked as complete"; } /** * Verify that only those actions without on-postback="false" are executed when the * magic postback parameter (javax.faces.ViewState) is present in the request map. */ @Test(enabled = true) public void testPostbackConditionOnPageAction() { FacesContext facesContext = FacesContext.getCurrentInstance(); simulatePostback(facesContext); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test06.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/action-test06.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA" }); } /** * This test is here (and disabled) to demonstrate the old behavior. All page * actions would be executed regardless and navigations could cross page * declaration boundaries since the view id is changing mid-run (hence * resulting in different navigation rule matches) */ @Test(enabled = false) public void oldBehaviorTest() { FacesContext facesContext = FacesContext.getCurrentInstance(); TestActions testActions = TestActions.instance(); facesContext.getViewRoot().setViewId("/action-test99a.xhtml"); Pages.instance().preRender(facesContext); assertViewId(facesContext, "/pageB.xhtml"); assertActionCalls(testActions, new String[] { "nonNullActionA", "nonNullActionB" }); } private void assertViewId(FacesContext facesContext, String expectedViewId) { String actualViewId = facesContext.getViewRoot().getViewId(); assert expectedViewId.equals(actualViewId) : "Expected viewId to be " + expectedViewId + ", but got " + actualViewId; } private void assertActionCalls(TestActions testActions, String[] methodNames) { List<String> actionsCalled = testActions.getActionsCalled(); assert actionsCalled.size() == methodNames.length : "Expected " + methodNames.length + " action(s) to be called, but executed " + actionsCalled.size() + " action(s) instead"; String expectedMethodCalls = ""; for (int i = 0, len = methodNames.length; i < len; i++) { if (i > 0) { expectedMethodCalls += ", "; } expectedMethodCalls += methodNames[i]; } String actualMethodCalls = ""; for (int i = 0, len = actionsCalled.size(); i < len; i++) { if (i > 0) { actualMethodCalls += ", "; } actualMethodCalls += actionsCalled.get(i); } assert expectedMethodCalls.equals(actualMethodCalls) : "Expected actions to be called: " + expectedMethodCalls + "; actions actually called: " + actualMethodCalls; Contexts.getEventContext().remove(Component.getComponentName(TestActions.class)); } private void simulatePostback(FacesContext facesContext) { MockHttpServletRequest request = (MockHttpServletRequest) facesContext.getExternalContext().getRequest(); request.getParameters().put(ResponseStateManager.VIEW_STATE_PARAM, new String[] { "true" }); assert facesContext.getRenderKit().getResponseStateManager().isPostback(facesContext) == true; } @Scope(ScopeType.EVENT) @Name("org.jboss.seam.core.manager") @BypassInterceptors public static class NoRedirectFacesManager extends FacesManager { @Override public void redirect(String viewId, Map<String, Object> parameters, boolean includeConversationId, boolean includePageParams) { Contexts.getEventContext().set("lastRedirectViewId", viewId); // a lot of shit happens we don't need; the important part is that the // viewId is not changed on FacesContext, but the response is marked complete FacesContext.getCurrentInstance().responseComplete(); } } }