keycloak-aplcache

KEYCLOAK-4816 KEYCLOAK-4817 Move javascript tests to base

2/26/2018 6:49:05 AM

Changes

.travis.yml 10(+10 -0)

testsuite/integration-arquillian/test-apps/js-console/example-realm.json 88(+0 -88)

testsuite/integration-arquillian/test-apps/js-console/pom.xml 80(+0 -80)

testsuite/integration-arquillian/test-apps/js-console/README.md 17(+0 -17)

testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html 363(+0 -363)

testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/WEB-INF/web.xml 25(+0 -25)

testsuite/integration-arquillian/test-apps/js-database/pom.xml 74(+0 -74)

testsuite/integration-arquillian/test-apps/js-database/src/main/java/org/keycloak/example/oauth/CustomerService.java 61(+0 -61)

testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/keycloak.json 8(+0 -8)

testsuite/integration-arquillian/test-apps/js-database/src/main/webapp/WEB-INF/web.xml 46(+0 -46)

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java 245(+0 -245)

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSDatabaseTestApp.java 46(+0 -46)

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java 594(+0 -594)

testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/example/EAPJSConsoleExampleAdapterTest.java 11(+0 -11)

testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/EAP6JSConsoleExampleAdapterTest.java 11(+0 -11)

testsuite/integration-arquillian/tests/other/adapters/jboss/relative/eap/src/test/java/org/keycloak/testsuite/adapter/example/RelativeEAPJSConsoleExampleAdapterTest.java 9(+0 -9)

testsuite/integration-arquillian/tests/other/adapters/jboss/relative/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/RelativeWildflyJSConsoleExampleAdapterTest.java 9(+0 -9)

testsuite/integration-arquillian/tests/other/adapters/jboss/remote/src/test/java/org/keycloak/testsuite/adapter/example/RemoteJSConsoleExampleAdapterTest.java 11(+0 -11)

testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyJSConsoleExampleAdapterTest.java 13(+0 -13)

testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/src/test/java/org/keycloak/testsuite/adapter/example/Wildfly10JSConsoleExampleAdapterTest.java 13(+0 -13)

Details

.travis.yml 10(+10 -0)

diff --git a/.travis.yml b/.travis.yml
index 3a494b4..ee4b9ec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -22,6 +22,16 @@ jdk:
 
 install: true
 
+before_install:
+  - "export PHANTOMJS_VERSION=2.1.1"
+  - "phantomjs --version"
+  - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH"
+  - "phantomjs --version"
+  - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi"
+  - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi"
+  - "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi"
+  - "phantomjs --version"
+
 script: 
   - ./travis-run-tests.sh $TESTS
 
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
index 9c3b005..c0445c1 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
@@ -30,6 +30,10 @@
     <artifactId>integration-arquillian-testsuite-providers</artifactId>
     <name>Auth Server Services - Testsuite Providers</name>
 
+    <properties>
+        <js-adapter.version>${project.version}</js-adapter.version>
+    </properties>
+
     <dependencies>
 
         <!-- Keycloak deps for tests -->
@@ -81,5 +85,32 @@
                 <filtering>true</filtering>
             </resource>
         </resources>
+
+        <plugins>
+            <plugin>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-javascript-adapter</id>
+                        <phase>generate-resources</phase>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.keycloak</groupId>
+                                    <artifactId>keycloak-js-adapter</artifactId>
+                                    <version>${js-adapter.version}</version>
+                                    <type>jar</type>
+                                    <outputDirectory>${pom.basedir}/src/main/resources/javascript</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                            <includes>**/keycloak.js</includes>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
     </build>
 </project>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java
new file mode 100644
index 0000000..cb7ed2c
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestJavascriptResource.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.rest.resource;
+
+import org.keycloak.testsuite.rest.TestingResourceProvider;
+
+import javax.print.attribute.standard.Media;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * @author mhajas
+ */
+public class TestJavascriptResource {
+
+    @GET
+    @Path("/js/keycloak.js")
+    @Produces("application/javascript")
+    public String getJavascriptAdapter() throws IOException {
+        return resourceToString("/javascript/keycloak.js");
+    }
+
+    @GET
+    @Path("/index.html")
+    @Produces(MediaType.TEXT_HTML)
+    public String getJavascriptTestingEnvironment() throws IOException {
+        return resourceToString("/javascript/index.html");
+    }
+
+    @GET
+    @Path("/keycloak.json")
+    @Produces(MediaType.APPLICATION_JSON)
+    public String getKeycloakJSON() throws IOException {
+        return resourceToString("/javascript/keycloak.json");
+    }
+
+    private String resourceToString(String path) throws IOException {
+        InputStream is = TestingResourceProvider.class.getResourceAsStream(path);
+        BufferedReader buf = new BufferedReader(new InputStreamReader(is));
+        String line = buf.readLine();
+        StringBuilder sb = new StringBuilder();
+        while (line != null) {
+            sb.append(line).append("\n");
+            line = buf.readLine();
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 7b76083..1e5b9ab 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -62,6 +62,7 @@ import org.keycloak.testsuite.forms.PassThroughAuthenticator;
 import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
 import org.keycloak.testsuite.rest.representation.AuthenticatorState;
 import org.keycloak.testsuite.rest.resource.TestCacheResource;
+import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
 import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
 import org.keycloak.testsuite.runonserver.ModuleUtil;
 import org.keycloak.testsuite.runonserver.FetchOnServer;
@@ -759,6 +760,11 @@ public class TestingResourceProvider implements RealmResourceProvider {
         }
     }
 
+    @Path("/javascript")
+    public TestJavascriptResource getJavascriptResource() {
+        return new TestJavascriptResource();
+    }
+
     private RealmModel getRealmByName(String realmName) {
         RealmProvider realmProvider = session.getProvider(RealmProvider.class);
         return realmProvider.getRealmByName(realmName);
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html
new file mode 100644
index 0000000..b352c08
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/javascript/index.html
@@ -0,0 +1,76 @@
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ 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.
+  -->
+
+<html>
+<head>
+    <script src="js/keycloak.js"></script>
+</head>
+<body>
+
+<h2>Result</h2>
+<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="output"></pre>
+
+<h2>Events</h2>
+<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
+
+
+<script>
+     function showExpires() {
+        if (!keycloak.tokenParsed) {
+            output("Not authenticated");
+            return;
+        }
+
+        var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
+        o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
+
+        if (keycloak.refreshTokenParsed) {
+            o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
+            o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
+        }
+
+        output(o);
+    }
+
+    function showError() {
+        output("Error: " + getParameterByName("error") + "\n" + "Error description: " + getParameterByName("error_description"));
+    }
+
+    function getParameterByName(name, url) {
+        if (!url) url = window.location.href;
+        name = name.replace(/[\[\]]/g, "\\$&");
+        var regex = new RegExp("[?&#]" + name + "(=([^&#]*)|&|#|$)"),
+            results = regex.exec(url);
+        if (!results) return null;
+        if (!results[2]) return '';
+        return decodeURIComponent(results[2].replace(/\+/g, " "));
+    }
+
+    function output(data) {
+        if (typeof data === 'object') {
+            data = JSON.stringify(data, null, '  ');
+        }
+        document.getElementById('output').innerHTML = data;
+    }
+
+    function event(event) {
+        var e = document.getElementById('events').innerHTML;
+        document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
+    }
+</script>
+</body>
+</html>
diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml
index 5a33513..e6c76e8 100644
--- a/testsuite/integration-arquillian/test-apps/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/pom.xml
@@ -15,9 +15,7 @@
     <name>Test apps</name>
 
     <modules>
-        <module>js-console</module>
         <module>test-apps-dist</module>
-        <module>js-database</module>
         <module>photoz</module>
         <module>hello-world-authz-service</module>
         <module>servlet-authz</module>
diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
index e3200c5..a9cd806 100755
--- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
+++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
@@ -19,14 +19,6 @@
 
     <target name="all">
         <delete dir="target/test-apps"/>
-        <copy todir="target/test-apps/js-console" overwrite="true">
-            <fileset dir="../js-console">
-                <exclude name="**/target/**"/>
-                <exclude name="**/*.iml"/>
-                <exclude name="**/*.unconfigured"/>
-                <exclude name="**/subsystem-config.xml"/>
-            </fileset>
-        </copy>
         <copy todir="target/test-apps/photoz" overwrite="true">
             <fileset dir="../photoz">
                 <exclude name="**/target/**"/>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
index 55b9d3a..f3312d9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OAuthGrant.java
@@ -16,6 +16,7 @@
  */
 package org.keycloak.testsuite.auth.page.login;
 
+import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
@@ -38,8 +39,14 @@ public class OAuthGrant extends LoginActions {
         cancelButton.click();
     }
 
+
+    public boolean isCurrent(WebDriver driver1) {
+        if (driver1 == null) driver1 = driver;
+        return driver1.getPageSource().contains("Do you grant these access privileges");
+    }
+
     @Override
     public boolean isCurrent() {
-        return driver.getPageSource().contains("Do you grant these access privileges");
+        return isCurrent(null);
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index dc6bfaf..04daf8d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -286,4 +286,14 @@ public interface TestingResource {
     @Produces(MediaType.TEXT_PLAIN_UTF_8)
     String runOnServer(String runOnServer);
 
+    @GET
+    @Path("js/keycloak.js")
+    @Produces(MediaType.TEXT_HTML_UTF_8)
+    String getJavascriptAdapter();
+
+    @GET
+    @Path("/get-javascript-testing-environment")
+    @Produces(MediaType.TEXT_HTML_UTF_8)
+    String getJavascriptTestingEnvironment();
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 6ea17c6..497947d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -50,6 +50,7 @@ import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.IDToken;
 import org.keycloak.representations.RefreshToken;
 import org.keycloak.representations.idm.KeysMetadataRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
 import org.keycloak.util.BasicAuthHelper;
 import org.keycloak.util.JsonSerialization;
@@ -69,6 +70,8 @@ import java.security.KeyStore;
 import java.security.PublicKey;
 import java.util.*;
 
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
@@ -197,6 +200,10 @@ public class OAuthClient {
         origin = null;
     }
 
+    public void setDriver(WebDriver driver) {
+        this.driver = driver;
+    }
+
     public AuthorizationEndpointResponse doLogin(String username, String password) {
         openLoginForm();
         fillLoginForm(username, password);
@@ -204,6 +211,11 @@ public class OAuthClient {
         return new AuthorizationEndpointResponse(this);
     }
 
+    public AuthorizationEndpointResponse doLogin(UserRepresentation user) {
+
+        return doLogin(user.getUsername(), getPasswordOf(user));
+    }
+
     public void fillLoginForm(String username, String password) {
         WaitUtils.waitForPageToLoad();
         String src = driver.getPageSource();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java
new file mode 100644
index 0000000..fabedb8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/AbstractJavascriptTest.java
@@ -0,0 +1,180 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.JavascriptBrowser;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * @author mhajas
+ */
+public abstract class AbstractJavascriptTest extends AbstractAuthTest {
+
+    @FunctionalInterface
+    interface QuadFunction<T, U, V, W> {
+        void apply(T a, U b, V c, W d);
+    }
+
+    public static final String CLIENT_ID = "js-console";
+    public static final String REALM_NAME = "test";
+    public static final String SPACE_REALM_NAME = "Example realm";
+    public static final String JAVASCRIPT_URL = "/auth/realms/" + REALM_NAME + "/testing/javascript";
+    public static final String JAVASCRIPT_ENCODED_SPACE_URL = "/auth/realms/Example%20realm/testing/javascript";
+    public static final String JAVASCRIPT_SPACE_URL = "/auth/realms/Example realm/testing/javascript";
+    public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
+
+
+    @Drone
+    @JavascriptBrowser
+    protected WebDriver jsDriver;
+
+    protected JavascriptExecutor jsExecutor;
+
+    @Page
+    @JavascriptBrowser
+    protected OIDCLogin testRealmLoginPage;
+
+    @FindBy(id = "output")
+    @JavascriptBrowser
+    protected WebElement outputArea;
+
+    @FindBy(id = "events")
+    @JavascriptBrowser
+    protected WebElement eventsArea;
+
+    public static final UserRepresentation testUser;
+    public static final UserRepresentation unauthorizedUser;
+
+    static {
+        testUser = UserBuilder.create().username("test-user@localhost").password("password").build();
+        unauthorizedUser = UserBuilder.create().username("unauthorized").password("password").build();
+    }
+
+
+    @Before
+    public void beforeJavascriptTest() {
+        jsExecutor = (JavascriptExecutor) jsDriver;
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(updateRealm(RealmBuilder.create()
+                .name(REALM_NAME)
+                .roles(
+                        RolesBuilder.create()
+                                .realmRole(new RoleRepresentation("user", "", false))
+                                .realmRole(new RoleRepresentation("admin", "", false))
+                )
+                .user(
+                        UserBuilder.create()
+                                .username("test-user@localhost").password("password")
+                                .addRoles("user")
+                                .role("realm-management", "view-realm")
+                                .role("realm-management", "manage-users")
+                                .role("account", "view-profile")
+                                .role("account", "manage-account")
+                )
+                .user(
+                        UserBuilder.create()
+                                .username("unauthorized").password("password")
+                )
+                .client(
+                        ClientBuilder.create()
+                                .clientId(CLIENT_ID)
+                                .redirectUris(JAVASCRIPT_URL + "/*", JAVASCRIPT_ENCODED_SPACE_URL + "/*")
+                                .publicClient()
+                )
+                .accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY)
+                .testEventListener()
+        ));
+    }
+
+    protected <T> JavascriptStateValidator buildFunction(QuadFunction<T, WebDriver, Object, WebElement> f, T x) {
+        return (y,z,w) -> f.apply(x, y, z, w);
+    }
+
+    protected void setImplicitFlowForClient() {
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
+        ClientRepresentation client = clientResource.toRepresentation();
+        client.setImplicitFlowEnabled(true);
+        client.setStandardFlowEnabled(false);
+        clientResource.update(client);
+    }
+
+    protected void setStandardFlowForClient() {
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
+        ClientRepresentation client = clientResource.toRepresentation();
+        client.setImplicitFlowEnabled(false);
+        client.setStandardFlowEnabled(true);
+        clientResource.update(client);
+    }
+
+    protected abstract RealmRepresentation updateRealm(RealmBuilder builder);
+
+    protected void assertSuccessfullyLoggedIn(WebDriver driver1, Object output, WebElement events) {
+        buildFunction(this::assertOutputContains, "Init Success (Authenticated)").validate(driver1, output, events);
+        waitUntilElement(events).text().contains("Auth Success");
+    }
+
+    protected void assertInitNotAuth(WebDriver driver1, Object output, WebElement events) {
+        buildFunction(this::assertOutputContains, "Init Success (Not Authenticated)").validate(driver1, output, events);
+    }
+
+    protected void assertOnLoginPage(WebDriver driver1, Object output, WebElement events) {
+        waitUntilElement(By.tagName("body")).is().present();
+        assertCurrentUrlStartsWith(testRealmLoginPage, driver1);
+    }
+
+    public void assertOutputWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
+        waitUntilElement((WebElement) output).text().contains(value);
+    }
+
+    public void assertOutputContains(String value, WebDriver driver1, Object output, WebElement events) {
+        if (output instanceof WebElement) {
+            waitUntilElement((WebElement) output).text().contains(value);
+        } else {
+            Assert.assertThat((String) output, containsString(value));
+        }
+    }
+
+    public void assertEventsWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
+        waitUntilElement(events).text().contains(value);
+    }
+
+    public ResponseValidator assertResponseStatus(long status) {
+        return output -> Assert.assertThat(output, hasEntry("status", status));
+    }
+
+    public JavascriptStateValidator assertOutputContains(String text) {
+        return buildFunction(this::assertOutputContains, text);
+    }
+
+    public JavascriptStateValidator assertEventsContains(String text) {
+        return buildFunction(this::assertEventsWebElementContains, text);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java
new file mode 100644
index 0000000..10361a6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptAdapterTest.java
@@ -0,0 +1,517 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.account.Applications;
+import org.keycloak.testsuite.auth.page.login.OAuthGrant;
+import org.keycloak.testsuite.util.JavascriptBrowser;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.WebElement;
+
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.Math.toIntExact;
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * @author mhajas
+ */
+public class JavascriptAdapterTest extends AbstractJavascriptTest {
+
+    private String testAppUrl;
+    private JavascriptTestExecutor testExecutor;
+    private static int TIME_SKEW_TOLERANCE = 3;
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Page
+    @JavascriptBrowser
+    private Applications applicationsPage;
+
+    @Page
+    @JavascriptBrowser
+    private OAuthGrant oAuthGrantPage;
+
+    @Override
+    protected RealmRepresentation updateRealm(RealmBuilder builder) {
+        return builder.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY).build();
+    }
+
+    @Before
+    public void setDefaultEnvironment() {
+        testAppUrl = authServerContextRootPage + JAVASCRIPT_URL + "/index.html";
+
+        testRealmLoginPage.setAuthRealm(REALM_NAME);
+        oAuthGrantPage.setAuthRealm(REALM_NAME);
+        applicationsPage.setAuthRealm(REALM_NAME);
+
+        jsDriver.navigate().to(testAppUrl);
+        testExecutor = JavascriptTestExecutor.create(jsDriver, testRealmLoginPage);
+
+        waitUntilElement(outputArea).is().present();
+        assertCurrentUrlStartsWith(testAppUrl, jsDriver);
+
+        jsDriver.manage().deleteAllCookies();
+    }
+
+    private JSObjectBuilder defaultArguments() {
+        return JSObjectBuilder.create().defaultSettings();
+    }
+
+    private void assertOnTestAppUrl(WebDriver jsDriver, Object output, WebElement events) {
+        assertCurrentUrlStartsWith(testAppUrl, jsDriver);
+    }
+
+    @Test
+    public void testJSConsoleAuth() {
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .login(this::assertOnLoginPage)
+                .loginForm( UserBuilder.create().username("user").password("invalid-password").build(),
+                        (driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
+                .loginForm(UserBuilder.create().username("invalid-user").password("password").build(),
+                        (driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+                .logout(this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertInitNotAuth);
+    }
+
+    @Test
+    public void testRefreshToken() {
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .refreshToken(9999, assertOutputContains("Failed to refresh token"))
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+                .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+    }
+
+    @Test
+    public void testRefreshTokenIfUnder30s() {
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+                .refreshToken(30, assertOutputContains("Token not refreshed, valid for"))
+                .addTimeSkew(-5) // instead of wait move in time
+                .refreshToken(30, assertEventsContains("Auth Refresh Success"));
+    }
+
+    @Test
+    public void testGetProfile() {
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .getProfile(assertOutputContains("Failed to load profile"))
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn)
+                .getProfile((driver1, output, events) -> Assert.assertThat((Map<String, String>) output, hasEntry("username", testUser.getUsername())));
+    }
+
+    @Test
+    public void grantBrowserBasedApp() {
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), CLIENT_ID);
+        ClientRepresentation client = clientResource.toRepresentation();
+        client.setConsentRequired(true);
+        clientResource.update(client);
+
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, (driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1))
+                        // I am not sure why is this driver1 argument to isCurrent necessary, but I got exception without it
+                );
+
+        oAuthGrantPage.accept();
+
+        EventRepresentation loginEvent = events.expectLogin()
+                .client(CLIENT_ID)
+                .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
+                .detail(Details.REDIRECT_URI, testAppUrl)
+                .detail(Details.USERNAME, testUser.getUsername())
+                .assertEvent();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        testExecutor.init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+
+        applicationsPage.navigateTo();
+        events.expectCodeToToken(codeId, loginEvent.getSessionId()).client(CLIENT_ID).assertEvent();
+
+        applicationsPage.revokeGrantForApplication(CLIENT_ID);
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account")
+                .detail(Details.REVOKED_CLIENT, CLIENT_ID)
+                .assertEvent();
+
+        jsDriver.navigate().to(testAppUrl);
+        testExecutor.configure() // need to configure because we refreshed page
+                .init(defaultArguments(), this::assertInitNotAuth)
+                .login((driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1)));
+
+        // Clean
+        client.setConsentRequired(false);
+        clientResource.update(client);
+    }
+
+    @Test
+    public void implicitFlowTest() {
+        testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
+                .login(this::assertOnTestAppUrl)
+                .errorResponse(assertOutputContains("Implicit flow is disabled for the client"));
+
+        setImplicitFlowForClient();
+        jsDriver.navigate().to(testAppUrl);
+
+        testExecutor.init(defaultArguments(), this::assertInitNotAuth)
+                .login(this::assertOnTestAppUrl)
+                .errorResponse(assertOutputContains("Standard flow is disabled for the client"));
+        jsDriver.navigate().to(testAppUrl);
+
+        testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments().implicitFlow(), this::assertSuccessfullyLoggedIn);
+
+        setStandardFlowForClient();
+    }
+
+    @Test
+    public void testCertEndpoint() {
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .sendXMLHttpRequest(XMLHttpRequest.create()
+                                .url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
+                                .method("GET")
+                                .addHeader("Accept", "application/json")
+                                .addHeader("Authorization", "Bearer ' + keycloak.token + '"),
+                        assertResponseStatus(200));
+    }
+
+    @Test
+    public void implicitFlowQueryTest() {
+        setImplicitFlowForClient();
+        testExecutor.init(defaultArguments().implicitFlow().queryResponse(), this::assertInitNotAuth)
+                .login(((driver1, output, events) ->
+                        Assert.assertThat(driver1.getCurrentUrl(), containsString("Response_mode+%27query%27+not+allowed"))));
+        setStandardFlowForClient();
+    }
+
+    @Test
+    public void implicitFlowRefreshTokenTest() {
+        setImplicitFlowForClient();
+        testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+            .refreshToken(9999, assertOutputContains("Failed to refresh token"));
+        setStandardFlowForClient();
+    }
+
+    @Test
+    public void implicitFlowOnTokenExpireTest() {
+        RealmRepresentation realm = adminClient.realms().realm(REALM_NAME).toRepresentation();
+        Integer storeAccesTokenLifespan = realm.getAccessTokenLifespanForImplicitFlow();
+        realm.setAccessTokenLifespanForImplicitFlow(5);
+        adminClient.realms().realm(REALM_NAME).update(realm);
+
+        setImplicitFlowForClient();
+        testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+                .addTimeSkew(-5); // Move in time instead of wait
+
+        waitUntilElement(eventsArea).text().contains("Access token expired");
+
+        // Get to origin state
+        realm.setAccessTokenLifespanForImplicitFlow(storeAccesTokenLifespan);
+        adminClient.realms().realm(REALM_NAME).update(realm);
+        setStandardFlowForClient();
+    }
+
+    @Test
+    public void implicitFlowCertEndpoint() {
+        setImplicitFlowForClient();
+        testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertSuccessfullyLoggedIn)
+                .sendXMLHttpRequest(XMLHttpRequest.create()
+                                .url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
+                                .method("GET")
+                                .addHeader("Accept", "application/json")
+                                .addHeader("Authorization", "Bearer ' + keycloak.token + '"),
+                        assertResponseStatus(200));
+        setStandardFlowForClient();
+    }
+
+    @Test
+    public void testBearerRequest() {
+        XMLHttpRequest request = XMLHttpRequest.create()
+                .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
+                .method("GET")
+                .addHeader("Accept", "application/json")
+                .addHeader("Authorization", "Bearer ' + keycloak.token + '");
+
+        testExecutor.init(defaultArguments())
+                .sendXMLHttpRequest(request, assertResponseStatus(401))
+                .refresh();
+        if (!"phantomjs".equals(System.getProperty("js.browser"))) {
+            // I have no idea why, but this request doesn't work with phantomjs, it works in chrome
+            testExecutor.logInAndInit(defaultArguments(), unauthorizedUser, this::assertSuccessfullyLoggedIn)
+                    .sendXMLHttpRequest(request, output -> Assert.assertThat(output, hasEntry("status", 403L)))
+                    .logout(this::assertOnTestAppUrl)
+                    .refresh();
+        }
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .sendXMLHttpRequest(request, assertResponseStatus(200));
+    }
+
+    @Test
+    public void loginRequiredAction() {
+        try {
+            testExecutor.init(defaultArguments().loginRequiredOnLoad());
+            // This throws exception because when JavascriptExecutor waits for AsyncScript to finish
+            // it is redirected to login page and executor gets no response
+
+            throw new RuntimeException("Probably the login-required OnLoad mode doesn't work, because testExecutor should fail with error that page was redirected.");
+        } catch (WebDriverException ex) {
+            // should happen
+        }
+
+        testExecutor.loginForm(testUser, this::assertOnTestAppUrl)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+    }
+
+    @Test
+    public void testUpdateToken() {
+        XMLHttpRequest request = XMLHttpRequest.create()
+                .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
+                .method("GET")
+                .addHeader("Accept", "application/json")
+                .addHeader("Authorization", "Bearer ' + keycloak.token + '");
+
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .addTimeSkew(-33);
+        setTimeOffset(33);
+        testExecutor.refreshToken(5, assertEventsContains("Auth Refresh Success"));
+
+        setTimeOffset(67);
+        testExecutor.addTimeSkew(-34)
+                .sendXMLHttpRequest(request, assertResponseStatus(401))
+                .refreshToken(5, assertEventsContains("Auth Refresh Success"))
+                .sendXMLHttpRequest(request, assertResponseStatus(200));
+
+        setTimeOffset(0);
+    }
+
+    @Test
+    public void timeSkewTest() {
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
+                        is(
+                            both(greaterThan(0 - TIME_SKEW_TOLERANCE))
+                            .and(lessThan(TIME_SKEW_TOLERANCE))
+                        )
+                ));
+
+        setTimeOffset(40);
+
+        testExecutor.refreshToken(9999, assertEventsContains("Auth Refresh Success"))
+                .checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
+                        is(
+                            both(greaterThan(-40 - TIME_SKEW_TOLERANCE))
+                            .and(lessThan(-40 + TIME_SKEW_TOLERANCE))
+                        )
+                ));
+    }
+
+    @Test
+    public void testOneSecondTimeSkewTokenUpdate() {
+        setTimeOffset(1);
+
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+
+        try {
+            // The events element should contain "Auth logout" but we need to wait for it
+            // and text().not().contains() doesn't wait. With KEYCLOAK-4179 it took some time for "Auth Logout" to be present
+            waitUntilElement(eventsArea).text().contains("Auth Logout");
+
+            throw new RuntimeException("The events element shouldn't contain \"Auth Logout\" text");
+        } catch (TimeoutException e) {
+            // OK
+        }
+    }
+
+    @Test
+    public void testLocationHeaderInResponse() {
+        XMLHttpRequest request = XMLHttpRequest.create()
+                .url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/users")
+                .method("POST")
+                .content("JSON.stringify(JSON.parse('{\"emailVerified\" : false, \"enabled\" : true, \"username\": \"mhajas\", \"firstName\" :\"First\", \"lastName\":\"Last\",\"email\":\"email@redhat.com\", \"attributes\": {}}'))")
+                .addHeader("Accept", "application/json")
+                .addHeader("Authorization", "Bearer ' + keycloak.token + '")
+                .addHeader("Content-Type", "application/json; charset=UTF-8");
+
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .sendXMLHttpRequest(request, response -> {
+                            List<UserRepresentation> users = adminClient.realm(REALM_NAME).users().search("mhajas", 0, 1);
+                            assertEquals("There should be created user mhajas", 1, users.size());
+
+                            assertThat((String) response.get("responseHeaders"), containsString("location: " + authServerContextRootPage.toString() + "/auth/admin/realms/" + REALM_NAME + "/users/" + users.get(0).getId()));
+                        });
+    }
+
+    @Test
+    public void spaceInRealmNameTest() {
+        // Unfortunately this test doesn't work on phantomjs
+        // it looks like phantomjs double encode %20 => %25%20
+        Assume.assumeTrue("This test doesn't work with phantomjs", !"phantomjs".equals(System.getProperty("js.browser")));
+
+        adminClient.realm(REALM_NAME).update(RealmBuilder.edit(adminClient.realm(REALM_NAME).toRepresentation()).name(SPACE_REALM_NAME).build());
+
+        JSObjectBuilder configuration = JSObjectBuilder.create()
+                .add("url", authServerContextRootPage + "/auth")
+                .add("realm", SPACE_REALM_NAME)
+                .add("clientId", CLIENT_ID);
+
+        testAppUrl = authServerContextRootPage + JAVASCRIPT_SPACE_URL + "/index.html";
+        jsDriver.navigate().to(testAppUrl);
+        testRealmLoginPage.setAuthRealm(SPACE_REALM_NAME);
+
+        testExecutor.configure(configuration)
+                .init(defaultArguments(), this::assertInitNotAuth)
+                .login(this::assertOnLoginPage)
+                .loginForm(testUser, this::assertOnTestAppUrl)
+                .configure(configuration)
+                .init(defaultArguments(), this::assertSuccessfullyLoggedIn);
+
+        // Clean
+        adminClient.realm(SPACE_REALM_NAME).update(RealmBuilder.edit(adminClient.realm(SPACE_REALM_NAME).toRepresentation()).name(REALM_NAME).build());
+        testRealmLoginPage.setAuthRealm(REALM_NAME);
+    }
+
+    @Test
+    public void initializeWithTokenTest() {
+        oauth.setDriver(jsDriver);
+
+        oauth.realm(REALM_NAME);
+        oauth.clientId(CLIENT_ID);
+        oauth.redirectUri(testAppUrl);
+        oauth.doLogin(testUser);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        String token = tokenResponse.getAccessToken();
+        String refreshToken = tokenResponse.getRefreshToken();
+
+        testExecutor.init(JSObjectBuilder.create()
+                    .add("token", token)
+                    .add("refreshToken", refreshToken)
+                , this::assertSuccessfullyLoggedIn)
+                .refreshToken(9999, assertEventsContains("Auth Refresh Success"));
+
+
+        oauth.setDriver(driver);
+    }
+
+    @Test
+    public void initializeWithTimeSkew() {
+        oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
+
+        // Get access token and refresh token to initialize with
+        setTimeOffset(600);
+        oauth.realm(REALM_NAME);
+        oauth.clientId(CLIENT_ID);
+        oauth.redirectUri(testAppUrl);
+        oauth.doLogin(testUser);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        String token = tokenResponse.getAccessToken();
+        String refreshToken = tokenResponse.getRefreshToken();
+
+        // Perform test
+        testExecutor.init(JSObjectBuilder.create()
+                        .add("token", token)
+                        .add("refreshToken", refreshToken)
+                        .add("timeSkew", -600)
+                , this::assertSuccessfullyLoggedIn)
+                .checkTimeSkew((driver1, output, events) -> assertThat(output, equalTo(-600L)))
+                .refreshToken(9999, assertEventsContains("Auth Refresh Success"))
+                .checkTimeSkew((driver1, output, events) -> assertThat(output, equalTo(-600L)));
+
+        setTimeOffset(0);
+
+        oauth.setDriver(driver); // Clean
+    }
+
+    @Test
+    // KEYCLOAK-4503
+    public void initializeWithRefreshToken() {
+        oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
+
+        oauth.realm(REALM_NAME);
+        oauth.clientId(CLIENT_ID);
+        oauth.redirectUri(testAppUrl);
+        oauth.doLogin(testUser);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        String token = tokenResponse.getAccessToken();
+        String refreshToken = tokenResponse.getRefreshToken();
+
+        testExecutor.init(JSObjectBuilder.create()
+                        .add("refreshToken", refreshToken)
+                , (driver1, output, events) -> {
+            assertInitNotAuth(driver1, output, events);
+            waitUntilElement(events).text().not().contains("Auth Success");
+        });
+
+        oauth.setDriver(driver); // Clean
+    }
+
+    @Test
+    public void reentrancyCallbackTest() {
+        testExecutor.logInAndInit(defaultArguments(), testUser, this::assertSuccessfullyLoggedIn)
+                .executeAsyncScript(
+                        "var callback = arguments[arguments.length - 1];" +
+                        "keycloak.updateToken(60).success(function () {" +
+                        "       event(\"First callback\");" +
+                        "       keycloak.updateToken(60).success(function () {" +
+                        "          event(\"Second callback\");" +
+                        "          callback(\"Success\");" +
+                        "       });" +
+                        "    }" +
+                        ");"
+                        , (driver1, output, events) -> {
+                            waitUntilElement(events).text().contains("First callback");
+                            waitUntilElement(events).text().contains("Second callback");
+                            waitUntilElement(events).text().not().contains("Auth Logout");
+                        }
+                );
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java
new file mode 100644
index 0000000..3c9f1ea
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptStateValidator.java
@@ -0,0 +1,15 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.keycloak.models.KeycloakSession;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.io.Serializable;
+
+/**
+ * @author mhajas
+ */
+public interface JavascriptStateValidator extends Serializable {
+
+    void validate(WebDriver driver, Object output, WebElement events);
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java
new file mode 100644
index 0000000..8db7dee
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JavascriptTestExecutor.java
@@ -0,0 +1,251 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * @author mhajas
+ */
+public class JavascriptTestExecutor {
+    private WebDriver jsDriver;
+    private JavascriptExecutor jsExecutor;
+    private WebElement output;
+    private WebElement events;
+    private OIDCLogin loginPage;
+    private boolean configured;
+
+    public static JavascriptTestExecutor create(WebDriver driver, OIDCLogin loginPage) {
+        return new JavascriptTestExecutor(driver, loginPage);
+    }
+
+    private JavascriptTestExecutor(WebDriver driver, OIDCLogin loginPage) {
+        this.jsDriver = driver;
+        driver.manage().timeouts().setScriptTimeout(10, TimeUnit.SECONDS);
+        jsExecutor = (JavascriptExecutor) driver;
+        events = driver.findElement(By.id("events"));
+        output = driver.findElement(By.id("output"));
+        this.loginPage = loginPage;
+        configured = false;
+    }
+
+    public JavascriptTestExecutor login() {
+        return login(null);
+    }
+
+    public JavascriptTestExecutor login(JavascriptStateValidator validator) {
+        jsExecutor.executeScript("keycloak.login()");
+
+        if (validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        configured = false; // Getting out of testApp page => loosing keycloak variable etc.
+
+        return this;
+    }
+
+    public JavascriptTestExecutor loginForm(UserRepresentation user) {
+        return loginForm(user, null);
+    }
+
+    public JavascriptTestExecutor loginForm(UserRepresentation user, JavascriptStateValidator validator) {
+        loginPage.form().login(user);
+
+        if (validator != null) {
+            validator.validate(jsDriver, null, events);
+        }
+
+        configured = false; // Getting out of testApp page => loosing keycloak variable etc.
+        // this is necessary in case we skipped login button for example in login-required mode
+
+        return this;
+    }
+
+    public JavascriptTestExecutor logout() {
+        return logout(null);
+    }
+
+    public JavascriptTestExecutor logout(JavascriptStateValidator validator) {
+        jsExecutor.executeScript("keycloak.logout()");
+        if (validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        configured = false; // Loosing keycloak variable so we need to create it when init next session
+
+        return this;
+    }
+
+    public JavascriptTestExecutor configure() {
+        return configure(null);
+    }
+
+    public JavascriptTestExecutor configure(JSObjectBuilder argumentsBuilder) {
+        if (argumentsBuilder == null) {
+            jsExecutor.executeScript("keycloak = Keycloak()");
+        } else {
+            String configArguments = argumentsBuilder.build();
+            jsExecutor.executeScript("keycloak = Keycloak(" + configArguments + ")");
+        }
+
+        jsExecutor.executeScript("keycloak.onAuthSuccess = function () {event('Auth Success')}"); // event function is declared in index.html
+        jsExecutor.executeScript("keycloak.onAuthError = function () {event('Auth Error')}");
+        jsExecutor.executeScript("keycloak.onAuthRefreshSuccess = function () {event('Auth Refresh Success')}");
+        jsExecutor.executeScript("keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')}");
+        jsExecutor.executeScript("keycloak.onAuthLogout = function () {event('Auth Logout')}");
+        jsExecutor.executeScript("keycloak.onTokenExpired = function () {event('Access token expired.')}");
+
+        configured = true;
+
+        return this;
+    }
+
+    public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder) {
+        return init(argumentsBuilder, null);
+    }
+
+    public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator) {
+        if(!configured) {
+            configure();
+        }
+
+        String arguments = argumentsBuilder.build();
+
+        Object output = jsExecutor.executeAsyncScript(
+                "var callback = arguments[arguments.length - 1];" +
+                "   keycloak.init(" + arguments + ").success(function (authenticated) {" +
+                "       callback(\"Init Success (\" + (authenticated ? \"Authenticated\" : \"Not Authenticated\") + \")\");" +
+                "   }).error(function () {" +
+                "       callback(\"Init Error\");" +
+                "   });");
+
+        if (validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        return this;
+    }
+
+    public JavascriptTestExecutor logInAndInit(JSObjectBuilder argumentsBuilder,
+                                               UserRepresentation user, JavascriptStateValidator validator) {
+        init(argumentsBuilder);
+        login();
+        loginForm(user);
+        init(argumentsBuilder, validator);
+        return this;
+    }
+
+    public JavascriptTestExecutor refreshToken(int value) {
+        return refreshToken(value, null);
+    }
+
+    public JavascriptTestExecutor refreshToken(int value, JavascriptStateValidator validator) {
+        Object output = jsExecutor.executeAsyncScript(
+                    "var callback = arguments[arguments.length - 1];" +
+                    "   keycloak.updateToken(" + Integer.toString(value) + ").success(function (refreshed) {" +
+                    "       if (refreshed) {" +
+                    "            callback(keycloak.tokenParsed);" +
+                    "       } else {" +
+                    "            callback('Token not refreshed, valid for ' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');" +
+                    "       }" +
+                    "   }).error(function () {" +
+                    "       callback('Failed to refresh token');" +
+                    "   });");
+
+        if(validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        return this;
+    }
+
+    public JavascriptTestExecutor getProfile() {
+        return getProfile(null);
+    }
+
+    public JavascriptTestExecutor getProfile(JavascriptStateValidator validator) {
+
+        Object output = jsExecutor.executeAsyncScript(
+                "var callback = arguments[arguments.length - 1];" +
+                "   keycloak.loadUserProfile().success(function (profile) {" +
+                "       callback(profile);" +
+                "   }).error(function () {" +
+                "       callback('Failed to load profile');" +
+                "   });");
+
+        if(validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+        return this;
+    }
+
+    public JavascriptTestExecutor sendXMLHttpRequest(XMLHttpRequest request, ResponseValidator validator) {
+        validator.validate(request.send(jsExecutor));
+
+        return this;
+    }
+
+    public JavascriptTestExecutor refresh() {
+        jsDriver.navigate().refresh();
+        configured = false; // Refreshing webpage => Loosing keycloak variable
+
+        return this;
+    }
+
+    public JavascriptTestExecutor addTimeSkew(int addition) {
+        jsExecutor.executeScript("keycloak.timeSkew += " + Integer.toString(addition));
+
+        return this;
+    }
+
+    public JavascriptTestExecutor checkTimeSkew(JavascriptStateValidator validator) {
+        Object timeSkew = jsExecutor.executeScript("return keycloak.timeSkew");
+
+        validator.validate(jsDriver, timeSkew, events);
+
+        return this;
+    }
+
+    public JavascriptTestExecutor executeScript(String script) {
+        return executeScript(script, null);
+    }
+
+    public JavascriptTestExecutor executeScript(String script, JavascriptStateValidator validator) {
+        Object output = jsExecutor.executeScript(script);
+
+        if(validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        return this;
+    }
+
+    public JavascriptTestExecutor executeAsyncScript(String script) {
+        return executeAsyncScript(script, null);
+    }
+
+    public JavascriptTestExecutor executeAsyncScript(String script, JavascriptStateValidator validator) {
+        Object output = jsExecutor.executeAsyncScript(script);
+
+        if(validator != null) {
+            validator.validate(jsDriver, output, events);
+        }
+
+        return this;
+    }
+
+    public JavascriptTestExecutor errorResponse(JavascriptStateValidator validator) {
+        Object output = jsExecutor.executeScript("return \"Error: \" + getParameterByName(\"error\") + \"\\n\" + \"Error description: \" + getParameterByName(\"error_description\")");
+
+        validator.validate(jsDriver, output, events);
+        return this;
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java
new file mode 100644
index 0000000..f0d549b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/JSObjectBuilder.java
@@ -0,0 +1,90 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public class JSObjectBuilder {
+
+    private Map<String, Object> arguments;
+
+
+    public static JSObjectBuilder create() {
+        return new JSObjectBuilder();
+    }
+
+    private JSObjectBuilder() {
+        arguments = new HashMap<>();
+    }
+
+    public JSObjectBuilder defaultSettings() {
+        standardFlow();
+        fragmentResponse();
+        return this;
+    }
+
+    public JSObjectBuilder standardFlow() {
+        arguments.put("flow", "standard");
+        return this;
+    }
+
+    public JSObjectBuilder implicitFlow() {
+        arguments.put("flow", "implicit");
+        return this;
+    }
+
+    public JSObjectBuilder fragmentResponse() {
+        arguments.put("responseMode", "fragment");
+        return this;
+    }
+
+    public JSObjectBuilder queryResponse() {
+        arguments.put("responseMode", "query");
+        return this;
+    }
+
+    public JSObjectBuilder checkSSOOnLoad() {
+        arguments.put("onLoad", "check-sso");
+        return this;
+    }
+
+    public JSObjectBuilder loginRequiredOnLoad() {
+        arguments.put("onLoad", "login-required");
+        return this;
+    }
+
+    public JSObjectBuilder add(String key, Object value) {
+        arguments.put(key, value);
+        return this;
+    }
+
+    public boolean isLoginRequired() {
+        return arguments.get("onLoad").equals("login-required");
+    }
+
+
+    public String build() {
+        StringBuilder argument = new StringBuilder("{");
+        String comma = "";
+        for (Map.Entry<String, Object> option : arguments.entrySet()) {
+            argument.append(comma)
+                    .append(option.getKey())
+                    .append(" : ");
+
+            if (!(option.getValue() instanceof Integer)) argument.append("\"");
+
+            argument.append(option.getValue());
+
+            if (!(option.getValue() instanceof Integer)) argument.append("\"");
+            comma = ",";
+        }
+
+        argument.append("}");
+
+        return argument.toString();
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java
new file mode 100644
index 0000000..8587d25
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/ResponseValidator.java
@@ -0,0 +1,15 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public interface ResponseValidator extends Serializable {
+
+    void validate(Map<String, Object> response);
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java
new file mode 100644
index 0000000..1a16611
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/javascript/XMLHttpRequest.java
@@ -0,0 +1,77 @@
+package org.keycloak.testsuite.adapter.javascript;
+
+import org.openqa.selenium.JavascriptExecutor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mhajas
+ */
+public class XMLHttpRequest {
+
+    private String url;
+    private String method;
+    private Map<String, String> headers;
+    private String content;
+
+    public static XMLHttpRequest create() {
+        return new XMLHttpRequest();
+    }
+
+    private XMLHttpRequest() {}
+
+    public XMLHttpRequest url(String url) {
+        this.url = url;
+        return this;
+    }
+
+    public XMLHttpRequest method(String method) {
+        this.method = method;
+        return this;
+    }
+
+    public XMLHttpRequest content(String content) {
+        this.content = content;
+        return this;
+    }
+
+    public XMLHttpRequest addHeader(String key, String value) {
+        if (headers == null) {
+            headers = new HashMap<>();
+        }
+
+        headers.put(key, value);
+
+        return this;
+    }
+
+    public Map<String, Object> send(JavascriptExecutor jsExecutor) {
+        String requestCode = "var callback = arguments[arguments.length - 1];" +
+                        "var req = new XMLHttpRequest();" +
+                        "        req.open('" + method + "', '" + url + "', true);" +
+                        getHeadersString() +
+                        "        req.onreadystatechange = function () {" +
+                        "            if (req.readyState == 4) {" +
+                        "                callback({\"status\" : req.status, \"reponseText\" : req.reponseText, \"responseHeaders\" : req.getAllResponseHeaders().toLowerCase(), \"res\" : req.response})" +
+                        "            }" +
+                        "        };" +
+                        "        req.send(" + content + ");";
+
+        return (Map<String, Object>) jsExecutor.executeAsyncScript(requestCode);
+    }
+
+    private String getHeadersString() {
+        StringBuilder builder = new StringBuilder();
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            builder.append("req.setRequestHeader('")
+                    .append(entry.getKey())
+                    .append("', '")
+                    .append(entry.getValue())
+                    .append("');");
+        }
+
+        return builder.toString();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 93ade25..d81d5cc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -44,14 +44,23 @@
         <!-- chrome -->
         <property name="chromeArguments">${chromeArguments}</property>
     </extension>
-    
+
     <extension qualifier="graphene">
         <property name="waitGuiInterval">5</property>
         <property name="waitAjaxInterval">5</property>
         <property name="waitModelInterval">10</property>
         <property name="waitGuardInterval">5</property>
     </extension>
-    
+
+    <extension qualifier="webdriver-javascriptbrowser">
+        <property name="browser">${js.browser}</property>
+        <property name="htmlUnit.version">${htmlUnitBrowserVersion}</property>
+        <property name="firefox_binary">${firefox_binary}</property>
+        <property name="chromeDriverBinary">${webdriver.chrome.driver}</property>
+        <property name="chromeArguments">${js.chromeArguments}</property>
+        <property name="phantomjs.cli.args">${phantomjs.cli.args} --ssl-certificates-path=${client.certificate.ca.path} --ssl-client-certificate-file=${client.certificate.file} --ssl-client-key-file=${client.key.file} --ssl-client-key-passphrase=${client.key.passphrase}</property>
+    </extension>
+
     <extension qualifier="graphene-secondbrowser">
         <property name="browser">${browser}</property>
         <property name="firefox_binary">${firefox_binary}</property>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index 2fc66a4..b7e9732 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -269,18 +269,6 @@
                                         <artifactItems>
                                             <artifactItem>
                                                 <groupId>org.keycloak.testsuite</groupId>
-                                                <artifactId>integration-arquillian-test-apps-js-console</artifactId>
-                                                <version>${project.version}</version>
-                                                <type>war</type>
-                                            </artifactItem>
-                                            <artifactItem>
-                                                <groupId>org.keycloak.testsuite</groupId>
-                                                <artifactId>integration-arquillian-test-apps-js-database</artifactId>
-                                                <version>${project.version}</version>
-                                                <type>war</type>
-                                            </artifactItem>
-                                            <artifactItem>
-                                                <groupId>org.keycloak.testsuite</groupId>
                                                 <artifactId>hello-world-authz-service</artifactId>
                                                 <version>${project.version}</version>
                                                 <type>war</type>
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 2af22be..1e29644 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -103,6 +103,8 @@
         <github.username/>
         <github.secretToken/>
         <ieDriverArch/>
+        <js.browser>phantomjs</js.browser>
+        <js.chromeArguments>--headless</js.chromeArguments>
         <htmlUnitBrowserVersion>chrome</htmlUnitBrowserVersion>
         <phantomjs.cli.args>--ignore-ssl-errors=true --web-security=false --ssl-certificates-path=${client.certificate.ca.path} --ssl-client-certificate-file=${client.certificate.file} --ssl-client-key-file=${client.key.file} --ssl-client-key-passphrase=${client.key.passphrase}</phantomjs.cli.args>
         <firefox_binary>/usr/bin/firefox</firefox_binary>
@@ -268,6 +270,8 @@
                             <test.intermittent>${test.intermittent}</test.intermittent>
 
                             <browser>${browser}</browser>
+                            <js.browser>${js.browser}</js.browser>
+                            <js.chromeArguments>${js.chromeArguments}</js.chromeArguments>
                             <htmlUnitBrowserVersion>${htmlUnitBrowserVersion}</htmlUnitBrowserVersion>
                             <webdriverDownloadBinaries>${webdriverDownloadBinaries}</webdriverDownloadBinaries>