Abstracting the interface between Browser.Command and Grizzly HTTP server
diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java b/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java
index 9b80d32..d433644 100644
--- a/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java
+++ b/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java
@@ -20,6 +20,7 @@
 
 import org.netbeans.html.presenters.render.Show;
 import java.io.Closeable;
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.Flushable;
 import java.io.IOException;
@@ -31,6 +32,9 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLDecoder;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -41,16 +45,9 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import org.glassfish.grizzly.PortRange;
-import org.glassfish.grizzly.http.server.HttpHandler;
-import org.glassfish.grizzly.http.server.HttpServer;
-import org.glassfish.grizzly.http.server.NetworkListener;
-import org.glassfish.grizzly.http.server.Request;
-import org.glassfish.grizzly.http.server.Response;
-import org.glassfish.grizzly.http.server.ServerConfiguration;
-import org.glassfish.grizzly.http.util.HttpStatus;
 import org.netbeans.html.boot.spi.Fn;
 import org.netbeans.html.boot.spi.Fn.Presenter;
 import org.netbeans.html.presenters.spi.ProtoPresenter;
@@ -74,12 +71,13 @@
 public final class Browser implements Fn.Presenter, Fn.KeepAlive, Flushable,
 Executor, Closeable {
     static final Logger LOG = Logger.getLogger(Browser.class.getName());
-    private final Map<String,Command> SESSIONS = new HashMap<String, Command>();
+    private final Map<String,Command> SESSIONS = new HashMap<>();
     private final String app;
-    private HttpServer s;
+    private HttpServer server;
     private Runnable onPageLoad;
     private Command current;
     private final Config config;
+    private final Supplier<HttpServer<?, ?, ?, ?>> serverProvider;
 
     /** Default constructor. Reads configuration from properties. The actual browser to
      * be launched can be influenced by value of
@@ -111,34 +109,39 @@
      * @param config the configuration
      */
     public Browser(Config config) {
-        this(findCalleeClassName(), config);
+        this(findCalleeClassName(), config, null);
     }
-    
-    Browser(String app, Config config) {
+
+    Browser(String app, Config config, Supplier<HttpServer<?,?,?, ?>> serverProvider) {
+        this.serverProvider = serverProvider != null ? serverProvider : GrizzlyServer::new;
         this.app = app;
         this.config = new Config(config);
     }
 
     @Override
     public final void execute(final Runnable r) {
-        current.runSafe(r, true);
+        current.execute(r);
     }
 
     @Override
     public void close() throws IOException {
-        s.shutdownNow();
+        if (server != null) {
+            server.shutdownNow();
+        }
     }
 
     HttpServer server() {
-        return s;
+        return server;
     }
 
     static HttpServer findServer(Object obj) {
-        Command c = null;
+        Command c;
         if (obj instanceof Command) {
             c = (Command) obj;
         } else if (obj instanceof ProtoPresenter) {
             c = ((ProtoPresenter) obj).lookup(Command.class);
+        } else {
+            throw new IllegalArgumentException("Cannot find server for " + obj);
         }
         return c.browser.server();
     }
@@ -199,23 +202,9 @@
     public void flush() throws IOException {
         throw new UnsupportedOperationException();
     }
-    
-    private static HttpServer server(RootPage r, Config config) {
-        int from = 8080;
-        int to = 65535;
-        int port = config.getPort();
-        if (port != -1) {
-            from = to = port;
-        }
-        HttpServer s = HttpServer.createSimpleServer(null, new PortRange(from, to));
-        final ServerConfiguration conf = s.getServerConfiguration();
-        conf.addHttpHandler(r, "/");
-        return s;
-    }
-    
+
     private static URI pageURL(String protocol, HttpServer server, final String page) {
-        NetworkListener listener = server.getListeners().iterator().next();
-        int port = listener.getPort();
+        int port = server.getPort();
         try {
             return new URI(protocol + "://localhost:" + port + page);
         } catch (URISyntaxException ex) {
@@ -227,22 +216,33 @@
     public final void displayPage(URL page, Runnable onPageLoad) {
         try {
             this.onPageLoad = onPageLoad;
-            s = server(new RootPage(page), config);
-            s.start();
-            show(pageURL("http", s, "/"));
+            this.server = serverProvider.get();
+            int from = 8080;
+            int to = 65535;
+            int port = config.getPort();
+            if (port != -1) {
+                from = to = port;
+            }
+            server.init(from, to);
+
+            this.server.addHttpHandler(new RootPage(page), "/");
+            server.start();
+
+            show(pageURL("http", server, "/"));
         } catch (IOException ex) {
             Logger.getLogger(Browser.class.getName()).log(Level.SEVERE, null, ex);
         }
     }
 
     /** Parameters to configure {@link Browser}.
-     * Create an instance and pass it 
+     * Create an instance and pass it
      * to {@link Browser#Browser(org.netbeans.html.presenters.browser.Browser.Config) }
      * constructor.
      */
     public final static class Config {
         String browser;
         Integer port;
+        boolean debug;
 
         /**
          * Default constructor.
@@ -253,6 +253,7 @@
         private Config(Config copy) {
             this.browser = copy.browser;
             this.port = copy.port;
+            this.debug = copy.debug;
         }
 
         /** The command to use when invoking a browser. Possible values:
@@ -288,7 +289,20 @@
             this.port = port;
             return this;
         }
-        
+
+        /** Enable or disable debugging. The default value is taken from a property
+         * {@code com.dukescript.presenters.browserDebug}. If the property is
+         * not specified, then the default value is {@code false}.
+         * 
+         * @param debug true or false
+         * @return this instance
+         * @since 1.8
+         */
+        Config debug(boolean debug) {
+            this.debug = debug;
+            return this;
+        }
+
         final String getBrowser() {
             if (browser != null) {
                 return browser;
@@ -300,40 +314,40 @@
             if (port != null) {
                 return port;
             }
-            String port = System.getProperty("com.dukescript.presenters.browserPort"); // NOI18N
+            String browserPort = System.getProperty("com.dukescript.presenters.browserPort"); // NOI18N
             try {
-                return Integer.parseInt(port);
+                return Integer.parseInt(browserPort);
             } catch (NumberFormatException ex) {
                 return -1;
             }
         }
     }
-    
-    static void cors(Response r) {
-        r.setCharacterEncoding("UTF-8");
-        r.addHeader("Access-Control-Allow-Origin", "*");
-        r.addHeader("Access-Control-Allow-Credentials", "true");
-        r.addHeader("Access-Control-Allow-Headers", "Content-Type");
-        r.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
+
+    static <Response> void cors(HttpServer<?, Response, ?, ?> s, Response r) {
+        s.setCharacterEncoding(r, "UTF-8");
+        s.addHeader(r, "Access-Control-Allow-Origin", "*");
+        s.addHeader(r, "Access-Control-Allow-Credentials", "true");
+        s.addHeader(r, "Access-Control-Allow-Headers", "Content-Type");
+        s.addHeader(r, "Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
     }
 
-    private final class RootPage extends HttpHandler {
+    private final class RootPage extends HttpServer.Handler {
         private final URL page;
 
         public RootPage(URL page) {
             this.page = page;
         }
-        
+
         @Override
-        public void service(Request rqst, Response rspns) throws Exception {
-            String path = rqst.getRequestURI();
-            cors(rspns);
+        public <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException {
+            String path = server.getRequestURI(rqst);
+            cors(server, rspns);
             if ("/".equals(path) || "index.html".equals(path)) {
                 Reader is;
-                String prefix = "http://" + rqst.getServerName() + ":" + rqst.getServerPort() + "/";
-                Writer w = rspns.getWriter();
-                rspns.setContentType("text/html");
-                final Command cmd = new Command(Browser.this, prefix);
+                String prefix = "http://" + server.getServerName(rqst) + ":" + server.getServerPort(rqst) + "/";
+                Writer w = server.getWriter(rspns);
+                server.setContentType(rspns, "text/html");
+                final Command cmd = new Command(server, Browser.this, prefix);
                 try {
                     is = new InputStreamReader(page.openStream());
                 } catch (IOException ex) {
@@ -378,11 +392,11 @@
                 is.close();
                 w.close();
             } else if (path.equals("/command.js")) {
-                String id = rqst.getParameter("id");
+                String id = server.getParameter(rqst, "id");
                 Command c = SESSIONS.get(id);
                 if (c == null) {
-                    rspns.getOutputBuffer().write("No command for " + id);
-                    rspns.setStatus(HttpStatus.NOT_FOUND_404);
+                    server.getWriter(rspns).write("No command for " + id);
+                    server.setStatus(rspns, 404);
                     return;
                 }
                 c.service(rqst, rspns);
@@ -392,13 +406,39 @@
                 }
                 URL relative = new URL(page, path);
                 InputStream is;
+                URLConnection conn;
                 try {
-                    is = relative.openStream();
+                    conn = relative.openConnection();
+                    is = conn.getInputStream();
                 } catch (FileNotFoundException ex) {
-                    rspns.setStatus(HttpStatus.NOT_FOUND_404);
+                    server.setStatus(rspns, 404);
                     return;
                 }
-                OutputStream out = rspns.getOutputStream();
+                String found = null;
+                if (relative.getProtocol().equals("file")) {
+                    try {
+                        File file = new File(relative.toURI());
+                        found = Files.probeContentType(file.toPath());
+                    } catch (URISyntaxException | IOException ignore) {
+                    }
+                } else {
+                    found = conn.getContentType();
+                }
+                if (found == null || "content/unknown".equals(found)) {
+                    if (path.endsWith(".html")) {
+                        found = "text/html";
+                    }
+                    if (path.endsWith(".js")) {
+                        found = "text/javascript";
+                    }
+                    if (path.endsWith(".css")) {
+                        found = "text/css";
+                    }
+                }
+                if (found != null) {
+                    server.setContentType(rspns, found);
+                }
+                OutputStream out = server.getOutputStream(rspns);
                 for (;;) {
                     int b = is.read();
                     if (b == -1) {
@@ -414,13 +454,19 @@
         private void emitScript(Writer w, String prefix, String id) throws IOException {
             w.write("  <script id='exec' type='text/javascript'>");
             w.write("\n"
-                    + "function waitForCommand() {\n"
+                    + "function waitForCommand(counter) {\n"
                     + "  try {\n"
                     + "    if (waitForCommand.seenError) {\n"
                     + "      console.warn('Disconnected from " + prefix + "');\n"
                     + "      return;\n"
                     + "    };\n"
-                    + "    var request = new XMLHttpRequest();\n"
+                    + "    var request = new XMLHttpRequest();\n");
+            if (Browser.this.config.debug) {
+                w.write(""
+                    + "    console.log('GET[' + counter + ']....');\n"
+                );
+            }
+            w.write(""
                     + "    request.open('GET', '" + prefix + "command.js?id=" + id + "', true);\n"
                     + "    request.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');\n"
                     + "    request.onerror = function(ev) {\n"
@@ -430,27 +476,73 @@
                     + "    request.onreadystatechange = function() {\n"
                     + "      if (this.readyState!==4) return;\n"
                     + "      try {\n"
+            );
+            if (Browser.this.config.debug) {
+                w.write(""
+                    + "        console.log('...GET[' + counter + '] got something ' + this.responseText.substring(0,80));\n"
                     + "        var cmd = document.getElementById('cmd');\n"
                     + "        if (cmd) cmd.innerHTML = this.responseText.substring(0,80);\n"
+                );
+            }
+            w.write(""
                     + "        (0 || eval)(this.responseText);\n"
                     + "      } catch (e) {\n"
                     + "        console.warn(e); \n"
                     + "      } finally {\n"
-                    + "        waitForCommand();\n"
+                    + "        waitForCommand(counter + 1);\n"
                     + "      }\n"
                     + "    };\n"
                     + "    request.send();\n"
                     + "  } catch (e) {\n"
                     + "    console.warn(e);\n"
-                    + "    waitForCommand();\n"
+                    + "    waitForCommand(counter + 1);\n"
                     + "  }\n"
                     + "}\n"
-                    + "waitForCommand();\n"
+                    + "waitForCommand(1);\n"
             );
             w.write("  </script>\n");
         }
     }
 
+    String createCallbackFn(String prefix, String id) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("this.toBrwsrSrvr = function(name, a1, a2, a3, a4) {\n"
+            + "var url = '").append(prefix).append("command.js?id=").append(id).append("&name=' + name;\n"
+            + "var body = 'p0=' + encodeURIComponent(a1);\n"
+            + "body += '&p1=' + encodeURIComponent(a2);\n"
+            + "body += '&p2=' + encodeURIComponent(a3);\n"
+            + "body += '&p3=' + encodeURIComponent(a4);\n"
+            + "var request = new XMLHttpRequest();\n"
+        );
+        if (Browser.this.config.debug) {
+            sb.append(""
+            + "console.log('PUT ... ' + body.substring(0, 80));\n"
+            + "var now = new Date().getTime();\n"
+            );
+        }
+        sb.append(""
+            + "request.open('PUT', url, false);\n"
+            + "request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');\n"
+            + "request.send(body);\n"
+            + "var txt = request.responseText;\n"
+        );
+        if (Browser.this.config.debug) {
+            sb.append(""
+            + "var then = new Date().getTime();\n"
+            + "if (txt && txt !== 'null') {\n"
+            + "  var cmd = document.getElementById('cmd');\n"
+            + "  if (cmd) cmd.innerHTML = txt.substring(0,80);\n"
+            + "}\n"
+            + "console.log('... PUT [' + (then - now) + 'ms]: ' + txt.substring(0, 80));\n"
+            );
+        }
+        sb.append(""
+            + "return txt;\n"
+            + "};\n"
+        );
+        return sb.toString();
+    }
+
     private static String findCalleeClassName() {
         StackTraceElement[] frames = new Exception().getStackTrace();
         for (StackTraceElement e : frames) {
@@ -477,22 +569,23 @@
         }
         return "org.netbeans.html"; // NOI18N
     }
-    
-    private static final class Command extends Object
-    implements Executor, ThreadFactory {
+
+    private static final class Command<Request, Response, Runner> extends Object
+    implements Executor {
+        private final HttpServer<Request, Response, ?, Runner> server;
         private final Queue<Object> exec;
         private final Browser browser;
         private final String id;
         private final String prefix;
-        private final Executor RUN;
-        private Thread RUNNER;
+        private Runner RUNNER;
         private Response suspended;
         private boolean initialized;
         private final ProtoPresenter presenter;
 
-        Command(Browser browser, String prefix) {
-            this.RUN = Executors.newSingleThreadExecutor(this);
+        Command(HttpServer<Request, Response, ?, Runner> s, Browser browser, String prefix) {
+            this.server = s;
             this.id = UUID.randomUUID().toString();
+            this.RUNNER = s.initializeRunner(this.id);
             this.exec = new LinkedList<>();
             this.prefix = prefix;
             this.browser = browser;
@@ -509,81 +602,54 @@
         }
 
         @Override
-        public Thread newThread(Runnable r) {
-            Thread t = new Thread(r, "Processor for " + id);
-            RUNNER = t;
-            return t;
+        public final void execute(final Runnable r) {
+            server.runSafe(this.RUNNER, r, this.presenter);
         }
 
-        @Override
-        public final void execute(final Runnable r) {
-            runSafe(r, true);
-        }
-        
-        final void runSafe(final Runnable r, final boolean context) {
-            class Wrap implements Runnable {
-                @Override
-                public void run() {
-                    if (context) {
-                        Closeable c = Fn.activate(Command.this.presenter);
-                        try {
-                            r.run();
-                        } finally {
-                            try {
-                                c.close();
-                            } catch (IOException ex) {
-                                // ignore
-                            }
-                        }
-                    } else {
-                        r.run();
-                    }
-                }
-            }
-            if (RUNNER == Thread.currentThread()) {
-                if (context) {
-                    Runnable w = new Wrap();
-                    w.run();
-                } else {
-                    r.run();
-                }
-            } else {
-                Runnable w = new Wrap();
-                RUN.execute(w);
-            }
-        }
-        
         final synchronized void add(Object obj) {
             if (suspended != null) {
-                try {
-                    suspended.getWriter().write(obj.toString());
-                } catch (IOException ex) {
-                    LOG.log(Level.SEVERE, null, ex);
-                }
-                suspended.resume();
+                Response rqst = suspended;
+                server.resume(rqst, () -> {
+                    try (Writer w = server.getWriter(rqst)) {
+                        w.write(obj.toString());
+                    } catch (IOException ex) {
+                        LOG.log(Level.SEVERE, null, ex);
+                    }
+                });
                 suspended = null;
                 return;
             }
             exec.add(obj);
         }
-        
+
         private synchronized Object take(Response rspns) {
             Object o = exec.poll();
             if (o != null) {
                 return o;
             }
             suspended = rspns;
-            rspns.suspend();
+            server.suspend(rspns);
             return null;
         }
-        
-        void service(Request rqst, Response rspns) throws Exception {
-            final String methodName = rqst.getParameter("name");
-            Writer w = rspns.getWriter();
+
+        private synchronized boolean initialize(Response rspns) {
+            if (!initialized) {
+                initialized = true;
+                suspended = rspns;
+                server.suspend(rspns);
+                execute(browser.onPageLoad);
+                return true;
+            }
+            return false;
+        }
+
+        void service(Request rqst, Response rspns) throws IOException {
+            final String methodName = server.getParameter(rqst, "name");
+            server.setContentType(rspns, "text/javascript");
+            Writer w = server.getWriter(rspns);
             if (methodName == null) {
-                if (!initialized) {
-                    initialized = true;
-                    execute(browser.onPageLoad);
+                if (initialize(rspns)) {
+                    return;
                 }
                 // send new request
                 Object obj = take(rspns);
@@ -595,13 +661,10 @@
                 w.write(s);
                 LOG.log(Level.FINE, "Exec global: {0}", s);
             } else {
-                List<String> args = new ArrayList<String>();
-                for (;;) {
-                    String p = rqst.getParameter("p" + args.size());
-                    if (p == null) {
-                        break;
-                    }
-                    args.add(p);
+                List<String> args = new ArrayList<>();
+                String body = server.getBody(rqst);
+                for (String p : body.split("&")) {
+                    args.add(URLDecoder.decode(p.substring(3), "UTF-8"));
                 }
                 String res;
                 try {
@@ -623,19 +686,7 @@
         }
 
         void callbackFn(ProtoPresenterBuilder.OnPrepared onReady) {
-            StringBuilder sb = new StringBuilder();
-            sb.append("this.toBrwsrSrvr = function(name, a1, a2, a3, a4) {\n"
-                + "var url = '").append(prefix).append("command.js?id=").append(id).append("&name=' + name;\n"
-                + "url += '&p0=' + encodeURIComponent(a1);\n"
-                + "url += '&p1=' + encodeURIComponent(a2);\n"
-                + "url += '&p2=' + encodeURIComponent(a3);\n"
-                + "url += '&p3=' + encodeURIComponent(a4);\n"
-                + "var request = new XMLHttpRequest();\n"
-                + "request.open('GET', url, false);\n"
-                + "request.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');\n"
-                + "request.send();\n"
-                + "return request.responseText;\n"
-                + "};\n");
+            String sb = this.browser.createCallbackFn(prefix, id);
             add(sb);
             onReady.callbackIsPrepared("toBrwsrSrvr");
         }
@@ -652,7 +703,7 @@
             }
             return Level.FINE;
         }
-        
+
         void log(int priority, String msg, Object... args) {
             Level level = findLevel(priority);
 
@@ -668,12 +719,12 @@
         }
 
         void dispatch(Runnable r) {
-            runSafe(r, false);
+            server.runSafe(RUNNER, r, null);
         }
 
 
         public void displayPage(URL url, Runnable r) {
             throw new UnsupportedOperationException(url.toString());
         }
-    } // end of Command  
+    } // end of Command
 }
diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java b/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java
new file mode 100644
index 0000000..78b8d33
--- /dev/null
+++ b/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java
@@ -0,0 +1,207 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import org.glassfish.grizzly.PortRange;
+import org.glassfish.grizzly.http.io.InputBuffer;
+import org.glassfish.grizzly.http.server.HttpHandler;
+import org.glassfish.grizzly.http.server.Request;
+import org.glassfish.grizzly.http.server.Response;
+import org.netbeans.html.boot.spi.Fn;
+
+final class GrizzlyServer extends HttpServer<Request, Response, Object, GrizzlyServer.Context> {
+    private org.glassfish.grizzly.http.server.HttpServer server;
+
+    @Override
+    void init(int from, int to) throws IOException {
+        server = org.glassfish.grizzly.http.server.HttpServer.createSimpleServer(null, new PortRange(from, to));
+    }
+
+    @Override
+    void shutdownNow() {
+        server.shutdownNow();
+    }
+
+    @Override
+    void addHttpHandler(Handler r, String mapping) {
+        server.getServerConfiguration().addHttpHandler(new HttpHandler() {
+            @Override
+            public void service(Request request, Response response) throws Exception {
+                r.service(GrizzlyServer.this, request, response);
+            }
+        }, mapping);
+    }
+
+    @Override
+    int getPort() {
+        return server.getListeners().iterator().next().getPort();
+    }
+
+    @Override
+    void start() throws IOException {
+        server.start();
+    }
+
+    @Override
+    String getRequestURI(Request r) {
+        return r.getRequestURI();
+    }
+
+    @Override
+    String getServerName(Request r) {
+        return r.getServerName();
+    }
+
+    @Override
+    int getServerPort(Request r) {
+        return r.getServerPort();
+    }
+
+    @Override
+    String getParameter(Request r, String id) {
+        return r.getParameter(id);
+    }
+
+    @Override
+    String getMethod(Request r) {
+        return r.getMethod().getMethodString();
+    }
+
+    @Override
+    String getBody(Request r) throws IOException {
+        final InputBuffer buffer = r.getInputBuffer();
+        buffer.processingChars();
+        buffer.fillFully(-1);
+        int len = buffer.availableChar();
+        char[] arr = new char[len];
+        int reallyRead = buffer.read(arr, 0, len);
+        assert reallyRead == len;
+        return new String(arr);
+    }
+
+    @Override
+    String getHeader(Request r, String header) {
+        return r.getHeader(header);
+    }
+
+    @Override
+    Writer getWriter(Response r) {
+        return r.getWriter();
+    }
+
+    @Override
+    void setContentType(Response r, String type) {
+        r.setContentType(type);
+    }
+
+    @Override
+    void setStatus(Response r, int code) {
+        r.setStatus(code);
+    }
+
+    @Override
+    OutputStream getOutputStream(Response r) {
+        return r.getOutputStream();
+    }
+
+    @Override
+    void suspend(Response r) {
+        r.suspend();
+    }
+
+    @Override
+    void resume(Response r, Runnable whenReady) {
+        whenReady.run();
+        r.resume();
+    }
+
+    @Override
+    void setCharacterEncoding(Response r, String set) {
+        r.setCharacterEncoding(set);
+    }
+
+    @Override
+    void addHeader(Response r, String name, String value) {
+        r.addHeader(name, value);
+    }
+
+    @Override
+    <WebSocket> void send(WebSocket socket, String s) {
+    }
+
+    class Context implements ThreadFactory {
+        private final String id;
+        Executor RUN;
+        Thread RUNNER;
+
+        Context(String id) {
+            this.id = id;
+        }
+
+        @Override
+        public Thread newThread(Runnable r) {
+            Thread t = new Thread(r, "Processor for " + id);
+            RUNNER = t;
+            return t;
+        }
+    }
+
+    @Override
+    Context initializeRunner(String id) {
+        Context c = new Context(id);
+        c.RUN = Executors.newSingleThreadExecutor(c);
+        return c;
+    }
+
+    @Override
+    final void runSafe(Context c, final Runnable r, final Fn.Presenter presenter) {
+        class Wrap implements Runnable {
+            @Override
+            public void run() {
+                if (presenter != null) {
+                    try (Closeable c = Fn.activate(presenter)) {
+                        r.run();
+                    } catch (IOException ex) {
+                        // go on
+                    }
+                } else {
+                    r.run();
+                }
+            }
+        }
+        if (c.RUNNER == Thread.currentThread()) {
+            if (presenter != null) {
+                Runnable w = new Wrap();
+                w.run();
+            } else {
+                r.run();
+            }
+        } else {
+            Runnable w = new Wrap();
+            c.RUN.execute(w);
+        }
+    }
+}
diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java b/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java
new file mode 100644
index 0000000..17aad38
--- /dev/null
+++ b/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import org.netbeans.html.boot.spi.Fn;
+
+abstract class HttpServer<Request, Response, WebSocket, Runner> {
+    abstract void init(int from, int to) throws IOException;
+    abstract void start() throws IOException;
+    abstract void shutdownNow();
+    abstract void addHttpHandler(Handler h, String path);
+    abstract int getPort();
+
+    abstract String getRequestURI(Request r);
+    abstract String getServerName(Request r);
+    abstract int getServerPort(Request r);
+    abstract String getParameter(Request r, String id);
+    abstract String getMethod(Request r);
+    abstract String getBody(Request r) throws IOException;
+    abstract String getHeader(Request r, String substring);
+
+    abstract Writer getWriter(Response r);
+    abstract void setContentType(Response r, String texthtml);
+    abstract void setStatus(Response r, int i);
+    abstract OutputStream getOutputStream(Response r);
+    abstract void suspend(Response r);
+    abstract void resume(Response r, Runnable runWhenResponseIsReady);
+    abstract void setCharacterEncoding(Response r, String utF8);
+    abstract void addHeader(Response r, String accessControlAllowOrigin, String string);
+
+    abstract <WebSocket> void send(WebSocket socket, String s);
+
+    abstract Runner initializeRunner(String id);
+    abstract void runSafe(Runner runner, Runnable code, Fn.Presenter presenter);
+
+    static abstract class Handler {
+        abstract <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException;
+    }
+
+    static abstract class WebSocketApplication {
+        abstract <WebSocket> void onMessage(HttpServer<?, ?, WebSocket, ?> server, WebSocket socket, String text);
+    }
+}
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java
index 6ca28b0..d20ee9c 100644
--- a/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java
@@ -18,73 +18,19 @@
  */
 package org.netbeans.html.presenters.browser;
 
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executors;
-import net.java.html.boot.BrowserBuilder;
-import org.netbeans.html.boot.spi.Fn;
 import org.netbeans.html.json.tck.JavaScriptTCK;
 import org.netbeans.html.json.tck.KOTest;
 import org.testng.annotations.Factory;
 
 public class BrowserTest extends JavaScriptTCK {
-    private static Class<?> browserClass;
-    private static Fn.Presenter browserPresenter;
-    
     public BrowserTest() {
     }
 
     @Factory public static Object[] compatibilityTests() throws Exception {
-        final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser("BrowserTest", new Browser.Config())).
-            loadClass(BrowserTest.class).
-            loadPage("empty.html").
-            invoke("initialized");
-
-        Executors.newSingleThreadExecutor().submit(new Runnable() {
-            @Override
-            public void run() {
-                bb.showAndWait();
-            }
-        });
-
-        List<Object> res = new ArrayList<Object>();
-        Class<? extends Annotation> test = 
-            loadClass().getClassLoader().loadClass(KOTest.class.getName()).
-            asSubclass(Annotation.class);
-
-        Class[] arr = (Class[]) loadClass().getDeclaredMethod("tests").invoke(null);
-        for (Class c : arr) {
-            for (Method m : c.getMethods()) {
-                if (m.getAnnotation(test) != null) {
-                    res.add(new KOScript(browserPresenter, m));
-                }
-            }
-        }
+        List<Object> res = new ArrayList<>();
+        ServerFactories.collect("BrowserTest", res, KOTest.class, JavaScriptTCK::testClasses);
         return res.toArray();
     }
-    
-    public static Class[] tests() {
-        return testClasses();
-    }
-
-    static synchronized Class<?> loadClass() throws InterruptedException {
-        while (browserClass == null) {
-            BrowserTest.class.wait();
-        }
-        return browserClass;
-    }
-    
-    public static synchronized void ready(Class<?> browserCls) throws Exception {
-        browserClass = browserCls;
-        browserPresenter = Fn.activePresenter();
-        BrowserTest.class.notifyAll();
-    }
-    
-    public static void initialized() throws Exception {
-        Class<?> classpathClass = ClassLoader.getSystemClassLoader().loadClass(BrowserTest.class.getName());
-        Method m = classpathClass.getMethod("ready", Class.class);
-        m.invoke(null, BrowserTest.class);
-    }
 }
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java b/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java
index 0ebbed5..9ad1f18 100644
--- a/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java
@@ -23,43 +23,33 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.Reader;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import org.glassfish.grizzly.http.server.HttpHandler;
-import org.glassfish.grizzly.http.server.HttpServer;
-import org.glassfish.grizzly.http.server.NetworkListener;
-import org.glassfish.grizzly.http.server.Request;
-import org.glassfish.grizzly.http.server.Response;
-import org.glassfish.grizzly.http.server.ServerConfiguration;
-import org.glassfish.grizzly.websockets.WebSocket;
-import org.glassfish.grizzly.websockets.WebSocketApplication;
-import org.glassfish.grizzly.websockets.WebSocketEngine;
+import org.netbeans.html.presenters.browser.HttpServer.Handler;
+import org.netbeans.html.presenters.browser.HttpServer.WebSocketApplication;
 
-final class DynamicHTTP extends HttpHandler {
+final class DynamicHTTP extends Handler {
     private static final Logger LOG = Logger.getLogger(DynamicHTTP.class.getName());
     private static int resourcesCount;
-    private List<Resource> resources = new ArrayList<Resource>();
-    private final ServerConfiguration conf;
+    private final List<Resource> resources = new ArrayList<>();
     private final HttpServer server;
     
     DynamicHTTP(HttpServer s) {
         server = s;
-        conf = s.getServerConfiguration();
     }
     
     @Override
-    public void service(Request request, Response response) throws Exception {
-        if ("/dynamic".equals(request.getRequestURI())) {
-            String mimeType = request.getParameter("mimeType");
-            List<String> params = new ArrayList<String>();
+    public <Request, Response> void service(HttpServer<Request, Response, ?, ?> s, Request request, Response response) throws IOException {
+        if ("/dynamic".equals(s.getRequestURI(request))) {
+            String mimeType = s.getParameter(request, "mimeType");
+            List<String> params = new ArrayList<>();
             boolean webSocket = false;
             for (int i = 0;; i++) {
-                String p = request.getParameter("param" + i);
+                String p = s.getParameter(request, "param" + i);
                 if (p == null) {
                     break;
                 }
@@ -69,7 +59,7 @@
                 }
                 params.add(p);
             }
-            final String cnt = request.getParameter("content");
+            final String cnt = s.getParameter(request, "content");
             String mangle = cnt.replace("%20", " ").replace("%0A", "\n");
             ByteArrayInputStream is = new ByteArrayInputStream(mangle.getBytes("UTF-8"));
             URI url;
@@ -79,36 +69,27 @@
             } else {
                 url = registerResource(res);
             }
-            response.getWriter().write(url.toString());
-            response.getWriter().write("\n");
+            server.getWriter(response).write(url.toString());
+            server.getWriter(response).write("\n");
             return;
         }
 
         for (Resource r : resources) {
-            if (r.httpPath.equals(request.getRequestURI())) {
-                response.setContentType(r.httpType);
+            if (r.httpPath.equals(s.getRequestURI(request))) {
+                server.setContentType(response, r.httpType);
                 r.httpContent.reset();
                 String[] params = null;
                 if (r.parameters.length != 0) {
                     params = new String[r.parameters.length];
                     for (int i = 0; i < r.parameters.length; i++) {
-                        params[i] = request.getParameter(r.parameters[i]);
+                        params[i] = s.getParameter(request, r.parameters[i]);
                         if (params[i] == null) {
                             if ("http.method".equals(r.parameters[i])) {
-                                params[i] = request.getMethod().toString();
+                                params[i] = s.getMethod(request);
                             } else if ("http.requestBody".equals(r.parameters[i])) {
-                                Reader rdr = request.getReader();
-                                StringBuilder sb = new StringBuilder();
-                                for (;;) {
-                                    int ch = rdr.read();
-                                    if (ch == -1) {
-                                        break;
-                                    }
-                                    sb.append((char) ch);
-                                }
-                                params[i] = sb.toString();
+                                params[i] = s.getBody(request);
                             } else if (r.parameters[i].startsWith("http.header.")) {
-                                params[i] = request.getHeader(r.parameters[i].substring(12));
+                                params[i] = s.getHeader(request, r.parameters[i].substring(12));
                             }
                         }
                         if (params[i] == null) {
@@ -117,27 +98,26 @@
                     }
                 }
 
-                copyStream(r.httpContent, response.getOutputStream(), null, params);
+                copyStream(r.httpContent, server.getOutputStream(response), null, params);
             }
         }
     }
     
     private URI registerWebSocket(Resource r) {
-        WebSocketEngine.getEngine().register("", r.httpPath, new WS(r));
+  //      WebSocketEngine.getEngine().register("", r.httpPath, new WS(r));
         return pageURL("ws", server, r.httpPath);
     }
 
     private URI registerResource(Resource r) {
         if (!resources.contains(r)) {
             resources.add(r);
-            conf.addHttpHandler(this, r.httpPath);
+            server.addHttpHandler(this, r.httpPath);
         }
         return pageURL("http", server, r.httpPath);
     }
     
     private static URI pageURL(String proto, HttpServer server, final String page) {
-        NetworkListener listener = server.getListeners().iterator().next();
-        int port = listener.getPort();
+        int port = server.getPort();
         try {
             return new URI(proto + "://localhost:" + port + page);
         } catch (URISyntaxException ex) {
@@ -193,13 +173,13 @@
         }
 
         @Override
-        public void onMessage(WebSocket socket, String text) {
+        public <WebSocket> void onMessage(HttpServer<?,?, WebSocket, ?> server, WebSocket socket, String text) {
             try {
                 r.httpContent.reset();
                 ByteArrayOutputStream out = new ByteArrayOutputStream();
                 copyStream(r.httpContent, out, null, text);
                 String s = new String(out.toByteArray(), "UTF-8");
-                socket.send(s);
+                server.send(socket, s);
             } catch (IOException ex) {
                 LOG.log(Level.WARNING, "Error processing message " + text, ex);
             }
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java b/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java
new file mode 100644
index 0000000..0d8b979
--- /dev/null
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import net.java.html.js.JavaScriptBody;
+
+final class JavaScriptUtilities {
+    private JavaScriptUtilities() {
+    }
+
+    @JavaScriptBody(args = {  }, body =
+          "var h;"
+        + "if (!!window && !!window.location && !!window.location.href)\n"
+        + "  h = window.location.href;\n"
+        + "else "
+        + "  h = null;"
+        + "return h;\n"
+    )
+    static native String findBaseURL();
+
+    @JavaScriptBody(args = {"value"}, body = "document.getElementById('loaded').innerHTML = value;")
+    static native void setLoaded(String value);
+
+    @JavaScriptBody(args = {"ms"}, body = "window.setTimeout(function() { window.close(); }, ms);")
+    static native void closeSoon(int ms);
+
+}
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java b/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java
index 937454d..e2f5217 100644
--- a/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java
@@ -38,15 +38,19 @@
     private Object result;
     private Object inst;
     private int cnt;
+    private final String prefix;
+    private final Fn updateName;
 
-    KOScript(Fn.Presenter p, Method m) {
+    KOScript(Fn updateName, String prefix, Fn.Presenter p, Method m) {
+        this.updateName = updateName;
+        this.prefix = prefix;
         this.p = p;
         this.m = m;
     }
 
     @Override
     public String getTestName() {
-        return m.getDeclaringClass().getSimpleName() + "." + m.getName();
+        return prefix + ":" + m.getDeclaringClass().getSimpleName() + "." + m.getName();
     }
 
     @Test
@@ -77,6 +81,9 @@
     public void run() {
         Closeable c = Fn.activate(p);
         try {
+            if (updateName != null) {
+                updateName.invoke(null, getTestName());
+            }
             if (inst == null) {
                 inst = m.getDeclaringClass().newInstance();
             }
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java
index 241aeee..b3c7f14 100644
--- a/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java
@@ -18,36 +18,26 @@
  */
 package org.netbeans.html.presenters.browser;
 
-import org.netbeans.html.presenters.browser.Browser;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
 import java.util.logging.ConsoleHandler;
 import java.util.logging.Level;
-import java.util.logging.Logger;
 import net.java.html.BrwsrCtx;
-import net.java.html.boot.BrowserBuilder;
-import net.java.html.js.JavaScriptBody;
 import org.netbeans.html.boot.spi.Fn;
 import org.netbeans.html.context.spi.Contexts;
 import org.netbeans.html.json.spi.Technology;
 import org.netbeans.html.json.spi.Transfer;
 import org.netbeans.html.json.tck.KOTest;
 import org.netbeans.html.json.tck.KnockoutTCK;
-import org.glassfish.grizzly.http.server.HttpServer;
-import org.glassfish.grizzly.http.server.ServerConfiguration;
 import org.netbeans.html.ko4j.KO4J;
 import org.openide.util.lookup.ServiceProvider;
 import static org.testng.Assert.assertNotNull;
@@ -55,106 +45,29 @@
 
 @ServiceProvider(service = KnockoutTCK.class)
 public class KoBrowserTest extends KnockoutTCK {
-    private static final Logger LOG = Logger.getLogger(KoBrowserTest.class.getName());
-    private static Class<?> browserClass;
-    private static Fn.Presenter browserPresenter;
-    
     public KoBrowserTest() {
     }
 
-    static Object[] showBrwsr(URI uri, String cmd) throws IOException {
-        LOG.log(Level.INFO, "Showing {0}", uri);
-        if (cmd == null) {
-            try {
-                LOG.log(Level.INFO, "Trying Desktop.browse on {0} {2} by {1}", new Object[]{
-                    System.getProperty("java.vm.name"),
-                    System.getProperty("java.vm.vendor"),
-                    System.getProperty("java.vm.version"),});
-                java.awt.Desktop.getDesktop().browse(uri);
-                LOG.log(Level.INFO, "Desktop.browse successfully finished");
-                return null;
-            } catch (UnsupportedOperationException ex) {
-                LOG.log(Level.INFO, "Desktop.browse not supported: {0}", ex.getMessage());
-                LOG.log(Level.FINE, null, ex);
-            }
-        }
-        {
-            String cmdName = cmd == null ? "xdg-open" : cmd;
-            String[] cmdArr = {
-                cmdName, uri.toString()
-            };
-            LOG.log(Level.INFO, "Launching {0}", Arrays.toString(cmdArr));
-            final Process process = Runtime.getRuntime().exec(cmdArr);
-            return new Object[]{process, null};
-        }
-    }
-   
     @Factory public static Object[] compatibilityTests() throws Exception {
         Browser.LOG.setLevel(Level.FINE);
         Browser.LOG.addHandler(new ConsoleHandler());
-        
-        final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser("KoBrowserTest", new Browser.Config())).
-            loadClass(KoBrowserTest.class).
-            loadPage("empty.html").
-            invoke("initialized");
 
-        Executors.newSingleThreadExecutor().submit(new Runnable() {
-            @Override
-            public void run() {
-                bb.showAndWait();
-            }
-        });
-
-        List<Object> res = new ArrayList<Object>();
-        Class<? extends Annotation> test = 
-            loadClass().getClassLoader().loadClass(KOTest.class.getName()).
-            asSubclass(Annotation.class);
-
-        Class[] arr = (Class[]) loadClass().getDeclaredMethod("tests").invoke(null);
-
-        final HttpServer s = Browser.findServer(browserPresenter);
-        ServerConfiguration conf = s.getServerConfiguration();
-        conf.addHttpHandler(new DynamicHTTP(s), "/dynamic");
-        for (Class c : arr) {
-            for (Method m : c.getMethods()) {
-                if (m.getAnnotation(test) != null) {
-                    res.add(new KOScript(browserPresenter, m));
-                }
-            }
+        List<Object> res = new ArrayList<>();
+        Fn.Presenter[] all = ServerFactories.collect("KoBrowserTest", res, KOTest.class, KnockoutTCK::testClasses);
+        for (Fn.Presenter browserPresenter : all) {
+            final HttpServer s = Browser.findServer(browserPresenter);
+            s.addHttpHandler(new DynamicHTTP(s), "/dynamic");
         }
         return res.toArray();
     }
     
-    public static Class[] tests() {
-        return testClasses();
-    }
-
-    static synchronized Class<?> loadClass() throws InterruptedException {
-        while (browserClass == null) {
-            KoBrowserTest.class.wait();
-        }
-        return browserClass;
-    }
-    
-    public static synchronized void ready(Class<?> browserCls) throws Exception {
-        browserClass = browserCls;
-        browserPresenter = Fn.activePresenter();
-        KoBrowserTest.class.notifyAll();
-    }
-    
-    public static void initialized() throws Exception {
-        browserPresenter = Fn.activePresenter();
-        Class<?> classpathClass = ClassLoader.getSystemClassLoader().loadClass(KoBrowserTest.class.getName());
-        Method m = classpathClass.getMethod("ready", Class.class);
-        m.invoke(null, KoBrowserTest.class);
-    }
-
     @Override
     public BrwsrCtx createContext() {
         KO4J ko = new KO4J();
         Contexts.Builder b = Contexts.newBuilder();
         b.register(Technology.class, ko.knockout(), 7);
         b.register(Transfer.class, ko.transfer(), 7);
+        Fn.Presenter browserPresenter = Fn.activePresenter();
         assertNotNull(browserPresenter, "Presenter needs to be registered");
         b.register(Executor.class, (Executor)browserPresenter, 10);
         return b.build();
@@ -174,16 +87,13 @@
         return json;
     }
 
-    private Fn jsonFn;
     private Object putValue(Object json, String key, Object value) {
-        if (jsonFn == null) {
-            jsonFn = Fn.activePresenter().defineFn(
-                "if (json === null) json = new Object();"
-                + "if (key !== null) json[key] = value;"
-                + "return json;",
-                "json", "key", "value"
-            );
-        }
+        Fn jsonFn = Fn.activePresenter().defineFn(
+            "if (json === null) json = new Object();"
+            + "if (key !== null) json[key] = value;"
+            + "return json;",
+            "json", "key", "value"
+        );
         try {
             return jsonFn.invoke(null, json, key, value);
         } catch (Exception ex) {
@@ -191,16 +101,13 @@
         }
     }
     
-    private Fn executeScript;
     @Override
     public Object executeScript(String script, Object[] arguments) {
-        if (executeScript == null) {
-            executeScript = Fn.activePresenter().defineFn(
-                "var f = new Function(s); "
-              + "return f.apply(null, args);",
-              "s", "args"
-            );
-        }
+        Fn executeScript = Fn.activePresenter().defineFn(
+            "var f = new Function(s); "
+            + "return f.apply(null, args);",
+            "s", "args"
+        );
         try {
             return executeScript.invoke(null, script, arguments);
         } catch (Exception ex) {
@@ -208,20 +115,10 @@
         }
     }
 
-    @JavaScriptBody(args = {  }, body = 
-          "var h;"
-        + "if (!!window && !!window.location && !!window.location.href)\n"
-        + "  h = window.location.href;\n"
-        + "else "
-        + "  h = null;"
-        + "return h;\n"
-    )
-    private static native String findBaseURL();
-    
     @Override
     public URI prepareURL(String content, String mimeType, String[] parameters) {
         try {
-            final URL baseURL = new URL(findBaseURL());
+            final URL baseURL = new URL(JavaScriptUtilities.findBaseURL());
             StringBuilder sb = new StringBuilder();
             sb.append("/dynamic?mimeType=").append(mimeType);
             for (int i = 0; i < parameters.length; i++) {
@@ -236,9 +133,7 @@
             BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream()));
             URI connectTo = new URI(br.readLine());
             return connectTo;
-        } catch (IOException ex) {
-            throw new IllegalStateException(ex);
-        } catch (URISyntaxException ex) {
+        } catch (IOException | URISyntaxException ex) {
             throw new IllegalStateException(ex);
         }
     }
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java
new file mode 100644
index 0000000..4b38321
--- /dev/null
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.function.Supplier;
+import net.java.html.boot.BrowserBuilder;
+import org.netbeans.html.boot.spi.Fn;
+import org.testng.ITest;
+import org.testng.annotations.DataProvider;
+
+public final class ServerFactories {
+    private ServerFactories() {
+    }
+
+    @DataProvider(name = "serverFactories")
+    public static Object[][] serverFactories() {
+        Supplier<HttpServer<?,?,?,?>> grizzly = GrizzlyServer::new;
+        List<Object[]> arr = new ArrayList<>();
+        arr.add(new Object[] {"Default", null});
+        return arr.toArray(new Object[0][]);
+    }
+
+    static Fn.Presenter[] collect(
+        String browserName, Collection<? super ITest> res,
+        Class<? extends Annotation> test, Supplier<Class[]> tests
+    ) throws Exception {
+        final Object[][] factories = serverFactories();
+        Fn.Presenter[] arr = new Fn.Presenter[factories.length];
+        for (int i = 0; i < factories.length; i++) {
+            Object[] pair = factories[i];
+            arr[i] = collect(browserName, (String) pair[0], (Supplier<HttpServer<?,?,?,?>>) pair[1], res, test, tests);
+        }
+        return arr;
+    }
+
+    static Fn.Presenter collect(
+        String browserName, String prefix, Supplier<HttpServer<?,?,?,?>> serverProvider,
+        Collection<? super ITest> res,
+        Class<? extends Annotation> test, Supplier<Class[]> tests
+    ) throws Exception {
+        Fn.Presenter[] browserPresenter = { null };
+        Fn[] updateName = { null };
+        CountDownLatch cdl = new CountDownLatch(1);
+        final Browser.Config cfg = new Browser.Config().debug(true);
+        final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser(browserName, cfg, serverProvider)).
+            loadPage("empty.html").
+            loadFinished(() -> {
+                browserPresenter[0] = Fn.activePresenter();
+                updateName[0] = Fn.define(KOScript.class,
+                    "document.getElementsByTagName('h1')[0].innerHTML='" + browserName + "@' + t;",
+                    "t"
+                );
+                cdl.countDown();
+            });
+        Executors.newSingleThreadExecutor().submit(bb::showAndWait);
+        cdl.await();
+        Class[] arr = tests.get();
+        for (Class c : arr) {
+            for (Method m : c.getMethods()) {
+                if (m.getAnnotation(test) != null) {
+                    res.add(new KOScript(updateName[0], prefix, browserPresenter[0], m));
+                }
+            }
+        }
+        return browserPresenter[0];
+    }
+
+}
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java
new file mode 100644
index 0000000..1170334
--- /dev/null
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.function.Supplier;
+import net.java.html.boot.BrowserBuilder;
+import static org.netbeans.html.presenters.browser.JavaScriptUtilities.closeSoon;
+import static org.netbeans.html.presenters.browser.JavaScriptUtilities.setLoaded;
+import org.testng.Assert;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+import org.testng.annotations.Test;
+
+public class ServerMimeTypeTest {
+    @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories")
+    public void checkMimeTypes(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws Exception {
+        final Thread main = Thread.currentThread();
+        final int[] loaded = { 0 };
+
+        Browser server = new Browser(
+            "test", new Browser.Config().command("NONE"), serverProvider
+        );
+        BrowserBuilder builder = BrowserBuilder.newBrowser(server)
+            .loadPage("server.html")
+            .loadFinished(() -> {
+                setLoaded("" + ++loaded[0]);
+                closeSoon(5000);
+            });
+        builder.showAndWait();
+
+        int serverPort = server.server().getPort();
+        URL connect = new URL("http://localhost:" + serverPort);
+        InputStream is = connect.openStream();
+        Assert.assertNotNull(is, "Connection opened");
+        byte[] arr = new byte[4096];
+        int len = is.read(arr);
+        is.close();
+
+        final String page = new String(arr, 0, len, "UTF-8");
+        assertTrue(page.contains("<h1>Server</h1>"), "Server page loaded OK:\n" + page);
+
+        String cssType = new URL(connect, "test.css").openConnection().getContentType();
+        assertMimeType(cssType, "text/css");
+
+        String jsType = new URL(connect, "test.js").openConnection().getContentType();
+        assertMimeType(jsType, "*/javascript");
+
+        String jsMinType = new URL(connect, "test.min.js").openConnection().getContentType();
+        assertMimeType(jsMinType, "*/javascript");
+
+        URLConnection conn = new URL(connect, "non-existing.file").openConnection();
+        assertTrue(conn instanceof HttpURLConnection, "it is HTTP connection: " + conn);
+
+        HttpURLConnection httpConn = (HttpURLConnection) conn;
+        assertEquals(httpConn.getResponseCode(), 404, "Expecting not exist status");
+
+        server.close();
+        try {
+            HttpURLConnection url =  (HttpURLConnection) connect.openConnection();
+            url.setConnectTimeout(3000);
+            url.setReadTimeout(3000);
+            InputStream unavailable = url.getInputStream();
+            fail("Stream can no longer be opened: " + unavailable);
+        } catch (IOException ex) {
+            // OK
+        }
+    }
+
+    private void assertMimeType(String type, String exp) {
+        int semicolon = type.indexOf(';');
+        if (semicolon >= 0) {
+            type = type.substring(0, semicolon);
+        }
+        if (exp.startsWith("*")) {
+            assertTrue(type.endsWith(exp.substring(1)), "Expecting " + exp + " but was: " + type);
+        } else {
+            assertEquals(type, exp);
+        }
+    }
+}
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java
index e3e5274..331c17c 100644
--- a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java
@@ -33,7 +33,8 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import net.java.html.boot.BrowserBuilder;
-import net.java.html.js.JavaScriptBody;
+import static org.netbeans.html.presenters.browser.JavaScriptUtilities.closeSoon;
+import static org.netbeans.html.presenters.browser.JavaScriptUtilities.setLoaded;
 import org.testng.Assert;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
@@ -109,12 +110,6 @@
         }
     }
 
-    @JavaScriptBody(args = { "value" }, body = "document.getElementById('loaded').innerHTML = value;")
-    private static native void setLoaded(String value);
-
-    @JavaScriptBody(args = { "ms" }, body = "window.setTimeout(function() { window.close(); }, ms);")
-    private static native void closeSoon(int ms);
-
     private static void show(URI page) throws IOException {
         ExecutorService background = Executors.newSingleThreadExecutor();
         Future<Void> future = background.submit((Callable<Void>) () -> {
diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java
new file mode 100644
index 0000000..4dddb18
--- /dev/null
+++ b/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java
@@ -0,0 +1,270 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.presenters.browser;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import static org.testng.Assert.*;
+import org.testng.annotations.Test;
+
+public class SimpleServerTest {
+    public SimpleServerTest() {
+    }
+
+    @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories")
+    public void testConnectionToTheServer(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException {
+        if (serverProvider == null) {
+            return;
+        }
+        int min = 42343;
+        int max = 49343;
+        HttpServer<?, ?, ?, ?> server = serverProvider.get();
+        server.init(min, max);
+        server.addHttpHandler(new HttpServer.Handler() {
+            @Override
+            <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException {
+                assertEquals(server.getServerName(rqst), "localhost", "Connecting from localhost");
+                assertEquals(server.getServerPort(rqst), server.getPort(), "Connecting via local port");
+                assertEquals(server.getMethod(rqst), "GET", "Requesting GET");
+
+                server.setCharacterEncoding(rspns, "UTF-8");
+                server.setContentType(rspns, "text/x-test");
+                Browser.cors(server, rspns);
+                try (Writer w = server.getWriter(rspns)) {
+                    final String n = server.getParameter(rqst, "name");
+                    final String reply;
+                    switch (server.getRequestURI(rqst)) {
+                        case "/reply/hi": reply = "Ahoj " + n + "!"; break;
+                        case "/reply/tchus": reply = "Ciao " + n + "!"; break;
+                        default: reply = "What?";
+                    }
+                    w.write(reply);
+                }
+            }
+        }, "/reply");
+        server.start();
+
+        int realPort = server.getPort();
+        assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort);
+
+        final String baseUri = "http://localhost:" + realPort;
+        assertURL("Ahoj John!", baseUri, "/reply/hi?name=John");
+        assertURL("Ciao John!", baseUri, "/reply/tchus?name=John");
+
+        server.shutdownNow();
+
+    }
+
+    private static void assertURL(String msg, String baseUri, final String path) throws IOException, MalformedURLException {
+        URL url = new URL(baseUri + path);
+        URLConnection conn = url.openConnection();
+
+        final String contentAndAttribs = conn.getContentType();
+        assertNotNull(contentAndAttribs, "Content-Type specified");
+        int semicolon = contentAndAttribs.indexOf(';');
+        final String content = semicolon == -1 ? contentAndAttribs : contentAndAttribs.substring(0, semicolon);
+        assertEquals(content, "text/x-test");
+
+        byte[] arr = new byte[8192];
+        int len = conn.getInputStream().read(arr);
+        assertNotEquals(len, -1, "Something shall be read");
+
+        String txt = new String(arr, 0, len, StandardCharsets.UTF_8);
+        assertEquals(txt, msg, "Message from the handler delivered");
+
+        assertEquals(conn.getHeaderField("Access-Control-Allow-Origin"), "*");
+    }
+
+    @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories")
+    public void testHeadersAndBody(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException {
+        if (serverProvider == null) {
+            return;
+        }
+        int min = 42343;
+        int max = 49343;
+        HttpServer<?, ?, ?, ?> server = serverProvider.get();
+        server.init(min, max);
+        server.addHttpHandler(new HttpServer.Handler() {
+            @Override
+            <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException {
+                StringBuilder sb = new StringBuilder(server.getBody(rqst));
+
+                server.setCharacterEncoding(rspns, "UTF-8");
+                server.setContentType(rspns, "text/plain");
+                try (Writer w = server.getWriter(rspns)) {
+                    final String action = server.getHeader(rqst, "action");
+                    assertNotNull(action, "action is specified");
+                    String reply;
+                    switch (action) {
+                        case "reverse": reply = sb.reverse().toString(); break;
+                        case "upper": reply = sb.toString().toUpperCase(); break;
+                        default: reply = "What?";
+                    }
+                    w.write(reply);
+                }
+            }
+        }, "/action");
+        server.start();
+
+        int realPort = server.getPort();
+        assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort);
+
+        final String baseUri = "http://localhost:" + realPort;
+        assertReadURL("reverse", "Ahoj", baseUri, "johA");
+        assertReadURL("upper", "Ahoj", baseUri, "AHOJ");
+
+        server.shutdownNow();
+
+    }
+
+    private static void assertReadURL(String action, String data, String baseUri, final String exp) throws IOException, MalformedURLException {
+        URL url = new URL(baseUri + "/action");
+        URLConnection conn = url.openConnection();
+        conn.addRequestProperty("action", action);
+        conn.setDoOutput(true);
+        conn.connect();
+        conn.getOutputStream().write(data.getBytes());
+
+
+        final String contentAndAttribs = conn.getContentType();
+        assertNotNull(contentAndAttribs, "Content-Type specified");
+        int semicolon = contentAndAttribs.indexOf(';');
+        final String content = semicolon == -1 ? contentAndAttribs : contentAndAttribs.substring(0, semicolon);
+        assertEquals(content, "text/plain");
+
+        byte[] arr = new byte[8192 * 8];
+        int offset = 0;
+        for (;;) {
+            int len = conn.getInputStream().read(arr, offset, arr.length - offset);
+            if (len == -1) {
+                break;
+            }
+            offset += len;
+        }
+        assertNotEquals(offset, 0, "Something shall be read");
+
+        String txt = new String(arr, 0, offset, StandardCharsets.UTF_8);
+        assertEquals(txt, exp, "Message from the handler delivered");
+    }
+
+    @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories")
+    public void testWaitForData(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException {
+        if (serverProvider == null) {
+            return;
+        }
+        ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
+        int min = 32343;
+        int max = 33343;
+        HttpServer<?, ?, ?, ?> server = serverProvider.get();
+        server.init(min, max);
+
+        class HandlerImpl extends HttpServer.Handler {
+            @Override
+            <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException {
+                server.setCharacterEncoding(rspns, "UTF-8");
+                server.setContentType(rspns, "text/x-test");
+                Browser.cors(server, rspns);
+                server.suspend(rspns);
+                exec.schedule((Callable <Void>) () -> {
+                    server.resume(rspns, () -> {
+                        Writer w = server.getWriter(rspns);
+                        try {
+                            w.write("Finished!");
+                        } catch (IOException ex) {
+                            throw new IllegalStateException(ex);
+                        }
+                    });
+                    return null;
+                }, 1, TimeUnit.SECONDS);
+            }
+        }
+        server.addHttpHandler(new HandlerImpl(), "/async");
+        server.start();
+
+        int realPort = server.getPort();
+        assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort);
+
+        final String baseUri = "http://localhost:" + realPort;
+        assertURL("Finished!", baseUri, "/async");
+
+        exec.shutdown();
+        server.shutdownNow();
+    }
+
+    @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories")
+    public void testEnormousBody(String name, Supplier<HttpServer<?,?,?, ?>> serverProvider) throws IOException {
+        if (serverProvider == null) {
+            return;
+        }
+        ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
+        int min = 32343;
+        int max = 33343;
+        HttpServer<?, ?, ?, ?> server = serverProvider.get();
+        server.init(min, max);
+
+        String id = veryLongId();
+
+        class HandlerImpl extends HttpServer.Handler {
+            @Override
+            <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException {
+                server.setCharacterEncoding(rspns, "UTF-8");
+                server.setContentType(rspns, "text/plain");
+                Browser.cors(server, rspns);
+
+                assertEquals("lower", server.getHeader(rqst, "action"));
+
+                String gotId = server.getBody(rqst);
+                if (!gotId.equals(id)) {
+                    fail("Id as expected by " + server + " isn't the same " + id.length() + " != " + gotId.length());
+                }
+                server.getWriter(rspns).write(gotId.toLowerCase());
+            }
+        }
+        server.addHttpHandler(new HandlerImpl(), "/action");
+        server.start();
+
+        int realPort = server.getPort();
+        assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort);
+
+        final String baseUri = "http://localhost:" + realPort;
+        assertReadURL("lower", id, baseUri, id.toLowerCase());
+
+        exec.shutdown();
+        server.shutdownNow();
+    }
+
+    private static String veryLongId() {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < 10000; i++) {
+            final int max = 'Z' - 'A';
+            int ch = 'A' + (i % max);
+            sb.append((char) ch);
+        }
+        return sb.toString();
+    }
+}
diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css
new file mode 100644
index 0000000..51da6c0
--- /dev/null
+++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js
new file mode 100644
index 0000000..51da6c0
--- /dev/null
+++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js
new file mode 100644
index 0000000..51da6c0
--- /dev/null
+++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */